diff --git a/build.gradle b/build.gradle index e1ffda78..1008b24e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' + id 'com.apollographql.apollo3' version '4.0.0-beta.7' + } group = 'camp.nextstep.edu' diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index f6d7f746..14acee13 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -39,7 +39,6 @@ public class SecurityConfig { private final CustomSuccessHandler customSuccessHandler; private final UserDetailsServiceImpl userDetailsService; - @Autowired private OAuth2UserDetailServiceImpl oAuth2UserDetailServiceImpl; @@ -74,7 +73,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // 기본 경로 및 테스트 경로 http.authorizeHttpRequests((authorize) -> authorize - .requestMatchers(HttpMethod.GET, "/api/farm/**").permitAll() + .requestMatchers(HttpMethod.POST, "/api/farm/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/product/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/review/**").permitAll() .requestMatchers(HttpMethod.GET, "/health").permitAll() diff --git a/src/main/java/poomasi/domain/farm/entity/Farm.java b/src/main/java/poomasi/domain/farm/entity/Farm.java index 1b4921f5..22c7b6c1 100644 --- a/src/main/java/poomasi/domain/farm/entity/Farm.java +++ b/src/main/java/poomasi/domain/farm/entity/Farm.java @@ -17,6 +17,7 @@ import java.time.LocalDateTime; +import poomasi.domain.order.entity._farm.OrderedFarm; import poomasi.domain.review.entity.Review; @Entity @@ -77,7 +78,11 @@ public class Farm { @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true) @JoinColumn(name = "entityId") - List reviewList = new ArrayList<>(); + private List reviewList = new ArrayList<>(); + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ordered_farm_id") + private OrderedFarm orderedFarm; @Builder public Farm(Long id, String name, Long ownerId, String address, String addressDetail, Double latitude, Double longitude, String description, int experiencePrice, Integer maxCapacity, Integer maxReservation) { diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index a18c99c2..2292127a 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -7,7 +7,10 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLDelete; -import poomasi.domain.order.entity.Order; +import poomasi.domain.store.entity.Store; +import poomasi.domain.member._profile.entity.MemberProfile; +import poomasi.domain.store.entity.Store; +import poomasi.domain.order.entity._product.ProductOrder; import poomasi.domain.store.entity.Store; import poomasi.domain.member._profile.entity.MemberProfile; import poomasi.domain.store.entity.Store; @@ -59,7 +62,7 @@ public class Member { private LocalDateTime deletedAt; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) - private List orderLists; + private List productOrderLists; @Setter @Column(nullable = true) @@ -77,6 +80,11 @@ public Member(String name, String email, String password, LoginType loginType, R this.role = role; } + public Member(String email, Role role) { + this.email = email; + this.role = role; + } + public void setMemberProfile(MemberProfile memberProfile) { this.memberProfile = memberProfile; if (memberProfile != null) { 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/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/_farm/FarmAfterSales.java b/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java new file mode 100644 index 00000000..c573c2ca --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/entity/_farm/FarmAfterSales.java @@ -0,0 +1,6 @@ +package poomasi.domain.order._aftersales.entity._farm; + + + +public class FarmAfterSales { +} 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 new file mode 100644 index 00000000..41a2fe2a --- /dev/null +++ b/src/main/java/poomasi/domain/order/_aftersales/entity/_product/ProductAfterSalesDetail.java @@ -0,0 +1,101 @@ +package poomasi.domain.order._aftersales.entity._product; + +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.time.LocalDateTime; + +@Description("상품 판매 후 교환/환불/추소 history") +@Entity +@Getter +@Table(name="product_after_sales_detail") +@NoArgsConstructor +public class ProductAfterSalesDetail { + + @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; + + @ManyToOne + private OrderedProduct orderedProduct; + + @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 BigDecimal adjustAmount; + + @Description("취소/교환/환불 수량") + private Integer adjustmentQuantity; + + @Description("환불/교환/취소 요청 사유") + private String reason; + + @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/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/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/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/_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 84acdd44..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,8 +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.dto.response.PaymentResponse; -import poomasi.domain.order._payment.service.PaymentService; +import poomasi.domain.order._payment.service.ProductPaymentService; import java.io.IOException; @@ -18,27 +17,38 @@ @RequiredArgsConstructor public class PaymentController { - private final PaymentService paymentService; + private final ProductPaymentService productPaymentService; @Description("사전 결제 api") @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) @PostMapping("/pre-payment") - public void postPrepare(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IamportResponseException, IOException { - paymentService.portonePrePaymentRegister(paymentPreRegisterRequest); + public ResponseEntity postPrepare(PaymentPreRegisterRequest paymentPreRegisterRequest) throws IamportResponseException, IOException { + return ResponseEntity.ok( + productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + ); } - @Description("사후 결제(검증 api)") - @PostMapping("/validate") - public void validatePayment(PaymentWebHookRequest paymentWebHookRequest) throws IamportResponseException, IOException { - paymentService.portoneVerifyPostPayment(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("포트원 webhook + 동기화") - * */ + + @Description("포트원 웹훅 수신 api") + @PostMapping("/portone-webhook") + public void handleIamportWebhook(@RequestBody PaymentWebHookRequest paymentWebHookRequest) throws IamportResponseException, IOException { + productPaymentService.handlePortOneProductWebhookEvent(paymentWebHookRequest); + } + + + + /* + @GetMapping("/") @Secured("ROLE_CUSTOMER") @Description("결제 내역 단건 조회") @@ -55,5 +65,12 @@ public ResponseEntity getPaymentByOrderId(@RequestParam Long orderId){ return ResponseEntity.ok(paymentResponse); } + */ + } + +/**TODO : filter 만들어서 webhook URL에 대해 IP 검증해야 함 + *@Description("포트원 webhook + 동기화") + @PostMapping("/portone-webhook") + * */ \ No newline at end of file diff --git a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java index c2df4c06..24fb5aae 100644 --- a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java +++ b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentPreRegisterRequest.java @@ -3,5 +3,4 @@ import java.math.BigDecimal; public record PaymentPreRegisterRequest(String merchantUid, BigDecimal amount) { - } diff --git a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentValidateRequest.java b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentValidateRequest.java new file mode 100644 index 00000000..51639b27 --- /dev/null +++ b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentValidateRequest.java @@ -0,0 +1,4 @@ +package poomasi.domain.order._payment.dto.request; + +public record PaymentValidateRequest(String merchantUid, String amount) { +} diff --git a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java index a735aa9b..b9f68f75 100644 --- a/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java +++ b/src/main/java/poomasi/domain/order/_payment/dto/request/PaymentWebHookRequest.java @@ -1,5 +1,8 @@ package poomasi.domain.order._payment.dto.request; -public record PaymentWebHookRequest(String imp_uid, - String merchant_uid) { +import com.fasterxml.jackson.annotation.JsonProperty; + +public record PaymentWebHookRequest(@JsonProperty("imp_uid") String impUid, + @JsonProperty("merchant_uid") String merchantUid, + String status) { } diff --git a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java b/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java index 9f6d0fa6..14f8ff20 100644 --- a/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java +++ b/src/main/java/poomasi/domain/order/_payment/dto/response/PaymentResponse.java @@ -6,7 +6,6 @@ import java.math.BigDecimal; public record PaymentResponse(Long paymentId, - String merchantUid, BigDecimal totalPrice, BigDecimal discountPrice, BigDecimal finalPrice, @@ -15,7 +14,6 @@ public record PaymentResponse(Long paymentId, public static PaymentResponse fromEntity(Payment payment){ return new PaymentResponse( payment.getId(), - payment.getMerchantUid(), payment.getTotalPrice(), payment.getDiscountPrice(), payment.getFinalPrice(), 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 321faa88..0983c68f 100644 --- a/src/main/java/poomasi/domain/order/_payment/entity/Payment.java +++ b/src/main/java/poomasi/domain/order/_payment/entity/Payment.java @@ -1,12 +1,14 @@ package poomasi.domain.order._payment.entity; - import jakarta.persistence.*; import jdk.jfr.Description; import lombok.Getter; -import poomasi.domain.order.entity.Order; +import poomasi.domain.order.entity.PaymentStatus; +import poomasi.domain.order.entity._farm.FarmOrder; +import poomasi.domain.order.entity._product.ProductOrder; import java.math.BigDecimal; +import java.util.List; @Entity @Getter @@ -16,12 +18,25 @@ public class Payment { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Description("상품 총 가격") + @Column(name = "imp_uid") + @Description("아임포트 결제 imp_uid") + private String impUid; + + @OneToOne(mappedBy = "payment") + private ProductOrder productOrder; + + @Description("포트원 결제 금액") private BigDecimal totalPrice; @Description("할인 가격") private BigDecimal discountPrice; - + + @Description("사용 포인트") + private BigDecimal usedPoint; + + @Description("배송비") + private BigDecimal deliveryFee; + @Description("최종 가격") private BigDecimal finalPrice; @@ -29,11 +44,23 @@ public class Payment { @Enumerated(EnumType.STRING) private PaymentMethod paymentMethod; - @OneToOne - private Order order; + @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; + } - @Description("포트원에서 결제 식별을 위한 merchant_uid") - @Column(name = "merchant_uid" , updatable = false) - private String merchantUid; } 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 77d04d38..00000000 --- a/src/main/java/poomasi/domain/order/_payment/entity/PaymentState.java +++ /dev/null @@ -1,31 +0,0 @@ -package poomasi.domain.order._payment.entity; - -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 e5d33028..00000000 --- a/src/main/java/poomasi/domain/order/_payment/service/PaymentService.java +++ /dev/null @@ -1,149 +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.AccessToken; -import com.siot.IamportRestClient.response.IamportResponse; -import com.siot.IamportRestClient.response.Prepare; -import jdk.jfr.Description; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -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._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.Order; -import poomasi.domain.order.repository.OrderRepository; -import poomasi.domain.product._cart.service.CartService; -import poomasi.global.error.BusinessError; -import poomasi.global.error.BusinessException; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.List; - -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 OrderRepository orderRepository; - private final CartService cartService; - - @Description("포트원 api 호출을 위한 accessToken 발급 메서드") - private String getPortOneAccessToken() throws IOException, IamportResponseException { - IamportResponse authResponse = iamportClient.getAuth(); - String accessToken = authResponse.getResponse().getToken(); - return accessToken; - } - - @Description("사전 결제 등록") - 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 - @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(); - - Order order = orderRepository.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 validatePaymentConsistency(BigDecimal prepaymentAmount, BigDecimal postPaymentAmount){ - if (prepaymentAmount.compareTo(postPaymentAmount) != 0) { - return false; - } - return true; - } - - @Description("payment 상세 내역 조회를 위한 단건 api 호출") - public void getPaymentDetails(String merchantUid, Long orderId) throws IOException, IamportResponseException { - - } - - @Description("결제 취소 api") - public void cancelPayment(IamportResponse response) throws IOException, IamportResponseException{ - //true면 Uid, false면 merchantUid로 판단 - CancelData cancelData = new CancelData(response.getResponse().getMerchantUid(), false); - iamportClient.cancelPaymentByImpUid(cancelData); - } - - @Description("결제 환불 api") - public void processRefund() throws IOException, IamportResponseException{ - - } - - - @Description("결제 부분 환불 api 호출") - public void partialRefund() throws IOException, IamportResponseException { - - } - - 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/_refund/entity/Refund.java b/src/main/java/poomasi/domain/order/_refund/entity/Refund.java deleted file mode 100644 index 8b51294d..00000000 --- a/src/main/java/poomasi/domain/order/_refund/entity/Refund.java +++ /dev/null @@ -1,20 +0,0 @@ -package poomasi.domain.order._refund.entity; - - -import jakarta.persistence.*; -import poomasi.domain.order.entity.OrderProductDetails; - -@Entity -@Table(name="refund") -public class Refund { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - -/* @OneToOne(fetch = FetchType.LAZY) - private OrderProductDetails orderProductDetails;*/ - - private String refundReason; - -} diff --git a/src/main/java/poomasi/domain/order/_refund/entity/RefundStatus.java b/src/main/java/poomasi/domain/order/_refund/entity/RefundStatus.java deleted file mode 100644 index 69a08f7c..00000000 --- a/src/main/java/poomasi/domain/order/_refund/entity/RefundStatus.java +++ /dev/null @@ -1,29 +0,0 @@ -package poomasi.domain.order._refund.entity; - -public enum RefundStatus { - REQUESTED("환불 요청됨"), - PROCESSING("환불 처리 중"), - COMPLETED("환불 완료됨"), - REJECTED("환불 거절됨"); - - private final String description; - - RefundStatus(String description) { - this.description = description; - } - - public String getDescription() { - return description; - } -} - -/* -* REQUESTED, // 반품 요청됨 - APPROVED, // 반품 승인됨 - REJECTED, // 반품 거부됨 - RETURNED, // 상품이 반품됨 - REFUNDED, // 환불 완료됨 - CANCELLED, // 반품 요청이 취소됨 - IN_TRANSIT // 반품이 배송 중 - ; -* */ diff --git a/src/main/java/poomasi/domain/order/controller/OrderController.java b/src/main/java/poomasi/domain/order/controller/OrderController.java index 8a8c7a81..d36111e6 100644 --- a/src/main/java/poomasi/domain/order/controller/OrderController.java +++ b/src/main/java/poomasi/domain/order/controller/OrderController.java @@ -7,12 +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.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; @@ -23,31 +24,43 @@ @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("/pre-order") - public ResponseEntity createPreOrder(@AuthenticationPrincipal UserDetailsImpl user) throws IOException, IamportResponseException { - PaymentPreRegisterRequest paymentPreRegisterRequest = orderService.preOrderRegister(); + @PostMapping("/product/pre-order") + @Description("product 사전 결제") + public ResponseEntity createProductPreOrder(@RequestBody ProductOrderRegisterRequest productOrderRegisterRequest) throws IOException, IamportResponseException { + PaymentPreRegisterRequest paymentPreRegisterRequest = productOrderService.productPreOrderRegister(productOrderRegisterRequest); return ResponseEntity.ok( - paymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) ); } - - @Description("멤버의 결제 완료가 된 단건 주문 조회") + + @Secured({"ROLE_CUSTOMER", "ROLE_FARMER"}) + @PostMapping("/farm/pre-order") + @Description("farm 사전 결제") + public ResponseEntity createFarmPreOrder() throws IOException, IamportResponseException { + PaymentPreRegisterRequest paymentPreRegisterRequest = productOrderService.farmPreOrderRegister(); + return ResponseEntity.ok( + productPaymentService.portonePrePaymentRegister(paymentPreRegisterRequest) + ); + } + + @Description("멤버의 결제 완료가 된 단건 주문 조회. 특정 건만 조회") @GetMapping("/{orderId}") public ResponseEntity getAllOrdersByMember(@PathVariable Long orderId) { return ResponseEntity.ok( - orderService.findOrderByMemberId(orderId) + productOrderService.findOrderByMemberId(orderId) ); } - @Description("멤버의 결제 완료가 된 전체 주문 목록 조회") + @Description("멤버의 결제 완료가 된 전체 주문 목록 조회. 전체 주문 목록 조회") @GetMapping("/") public ResponseEntity getOrdersByMember() { return ResponseEntity.ok( - orderService.findAllOrdersByMemberId() + productOrderService.findAllOrdersByMemberId() ); } @@ -55,7 +68,7 @@ public ResponseEntity getOrdersByMember() { @GetMapping("/{orderId}/details") public ResponseEntity getOrderDetailsByMember(@PathVariable Long orderId) { return ResponseEntity.ok( - orderService.findOrderDetailsByOrderId(orderId) + productOrderService.findOrderDetailsByOrderId(orderId) ); } @@ -64,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) ); } @@ -72,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/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 c93a0358..749f9dab 100644 --- a/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java +++ b/src/main/java/poomasi/domain/order/dto/response/OrderDetailsResponse.java @@ -1,17 +1,17 @@ package poomasi.domain.order.dto.response; -import poomasi.domain.order.entity.OrderDetails; +import poomasi.domain.order.entity._product.ProductOrderDetails; public record OrderDetailsResponse( String address, String addressDetails, String deliveryRequest ) { - public static OrderDetailsResponse fromEntity(OrderDetails orderDetails) { + public static OrderDetailsResponse fromEntity(ProductOrderDetails productOrderDetails) { return new OrderDetailsResponse( - orderDetails.getAddress(), - orderDetails.getAddressDetail(), - orderDetails.getDeliveryRequest() + productOrderDetails.getDestinationAddress(), + productOrderDetails.getDestinationAddressDetail(), + productOrderDetails.getDeliveryRequest() ); } } diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java index 9e2ae945..1726d646 100644 --- a/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java +++ b/src/main/java/poomasi/domain/order/dto/response/OrderProductDetailsResponse.java @@ -1,7 +1,6 @@ package poomasi.domain.order.dto.response; -import poomasi.domain.order.entity.OrderProductDetails; -import poomasi.domain.product.dto.ProductResponse; +import poomasi.domain.order.entity._product.OrderedProduct; import java.math.BigDecimal; @@ -14,15 +13,15 @@ public record OrderProductDetailsResponse( BigDecimal price, //총 결제 금액 String invoiceNumber ) { - public static OrderProductDetailsResponse fromEntity(OrderProductDetails orderProductDetails) { + public static OrderProductDetailsResponse fromEntity(OrderedProduct orderedProduct) { return new OrderProductDetailsResponse( - orderProductDetails.getOrder().getId(), - orderProductDetails.getId(), - orderProductDetails.getProduct().getId(), - orderProductDetails.getProductName(), - orderProductDetails.getCount(), - orderProductDetails.getPrice(), - orderProductDetails.getInvoiceNumber() + orderedProduct.getOrderId(), + orderedProduct.getId(), + orderedProduct.getProduct().getId(), + orderedProduct.getProductName(), + orderedProduct.getCount(), + orderedProduct.getPrice(), + orderedProduct.getInvoiceNumber() ); } diff --git a/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java b/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java index 77f8a793..e5621032 100644 --- a/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java +++ b/src/main/java/poomasi/domain/order/dto/response/OrderResponse.java @@ -1,8 +1,6 @@ package poomasi.domain.order.dto.response; -import poomasi.domain.order._payment.dto.response.PaymentResponse; -import poomasi.domain.order.entity.Order; -import poomasi.domain.order.entity.OrderStatus; +import poomasi.domain.order.entity._product.ProductOrder; import java.time.LocalDateTime; import java.util.List; @@ -12,12 +10,12 @@ public record OrderResponse(Long orderId, String merchantUid, LocalDateTime createdAt, List orderProductDetailsResponseList) { - public static OrderResponse fromEntity(Order order) { + public static OrderResponse fromEntity(ProductOrder productOrder) { return new OrderResponse( - order.getId(), - order.getMerchantUid(), - order.getCreatedAt(), - order.getOrderProductDetails() + productOrder.getId(), + productOrder.getMerchantUid(), + productOrder.getCreatedAt(), + productOrder.getOrderedProducts() .stream() .map(OrderProductDetailsResponse::fromEntity) .collect(Collectors.toList()) diff --git a/src/main/java/poomasi/domain/order/entity/AbstractOrder.java b/src/main/java/poomasi/domain/order/entity/AbstractOrder.java deleted file mode 100644 index 82a7c4ff..00000000 --- a/src/main/java/poomasi/domain/order/entity/AbstractOrder.java +++ /dev/null @@ -1,33 +0,0 @@ -package poomasi.domain.order.entity; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import jdk.jfr.Timestamp; -import lombok.Getter; -import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.member.entity.Member; - -import java.time.LocalDateTime; -import java.util.Date; - -@MappedSuperclass -@Getter -public abstract class AbstractOrder { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(name = "member_id") - @ManyToOne(fetch = FetchType.LAZY) - private Member member; - - @Column(name = "merchant_uid") - @Description("상품당 결제 id(아임포트 id)") - private String merchantUid = "p" + new Date().getTime(); - - @Column(name = "created_at") - @Timestamp - private LocalDateTime createdAt = LocalDateTime.now(); - -} diff --git a/src/main/java/poomasi/domain/order/entity/Order.java b/src/main/java/poomasi/domain/order/entity/Order.java deleted file mode 100644 index a830bed2..00000000 --- a/src/main/java/poomasi/domain/order/entity/Order.java +++ /dev/null @@ -1,49 +0,0 @@ -package poomasi.domain.order.entity; - - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.util.List; - -import static poomasi.domain.order.entity.OrderStatus.PENDING; - -@Entity -@Table(name = "orders") -@Getter -@NoArgsConstructor -public class Order extends AbstractOrder{ - - @Column(name = "order_product_details_id") - @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) - private List orderProductDetails; - - @OneToOne - @JoinColumn(name = "order_details_id") // 여기서 JoinColumn 사용 - private OrderDetails orderDetails; - - @Column(name = "total_amount") - private BigDecimal totalAmount; - - @Enumerated(EnumType.STRING) - private OrderStatus orderStatus = OrderStatus.PENDING; - - public Order(OrderDetails orderDetails) { - this.orderDetails = orderDetails; - } - - public void addOrderDetail(OrderProductDetails orderProductDetails) { - this.orderProductDetails.add(orderProductDetails); - } - public void setOrderStatus(OrderStatus orderStatus) { - this.orderStatus = orderStatus; - } - - public void setTotalAmount(BigDecimal totalAmount) { - this.totalAmount = totalAmount; - } - - -} diff --git a/src/main/java/poomasi/domain/order/entity/OrderDetails.java b/src/main/java/poomasi/domain/order/entity/OrderDetails.java deleted file mode 100644 index 5bb7ed09..00000000 --- a/src/main/java/poomasi/domain/order/entity/OrderDetails.java +++ /dev/null @@ -1,38 +0,0 @@ -package poomasi.domain.order.entity; - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name="order_details") -@Getter -@NoArgsConstructor -public class OrderDetails { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @OneToOne(mappedBy = "orderDetails") - private Order order; - - @Column(name = "address") - private String address; - - @Column(name = "address_detail") - private String addressDetail; - - @Description("배송 요청 사항") - @Column(name = "delivery_request", length = 255) - private String deliveryRequest; - - public OrderDetails(String address, String addressDetail, String deliveryRequest) { - this.address = address; - this.addressDetail = addressDetail; - this.deliveryRequest = deliveryRequest; - } - - -} diff --git a/src/main/java/poomasi/domain/order/entity/OrderProductDetails.java b/src/main/java/poomasi/domain/order/entity/OrderProductDetails.java deleted file mode 100644 index 5be2e668..00000000 --- a/src/main/java/poomasi/domain/order/entity/OrderProductDetails.java +++ /dev/null @@ -1,71 +0,0 @@ -package poomasi.domain.order.entity; - - -import jakarta.persistence.*; -import jdk.jfr.Description; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import poomasi.domain.order._refund.entity.Refund; -import poomasi.domain.product.entity.Product; - -import java.io.Serializable; -import java.math.BigDecimal; - -@Entity -@Table(name = "order_product_details") -@Getter -@NoArgsConstructor -public class OrderProductDetails implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "order_product_details_id") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id") - private Product product; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "order_id") - private Order order; - - /* @OneToOne(fetch = FetchType.LAZY) - private Refund refund;*/ - - @Column(name = "product_description", nullable = true) - private String productDescription; - - @Column(name = "product_name", length = 255) - private String productName; - - @Description("구매 당시 1개당 가격") - private BigDecimal price; - - @Column(name="count") - private Integer count; - - @Description("송장 번호") - @Column(name = "invoice_number") - private String invoiceNumber; - - //private String sellerName; - //private OneToMany Review; - //refund.. - - @Builder - public OrderProductDetails(Product product, Order order, String productDescription, String productName, BigDecimal price, Integer count) { - this.product = product; - this.order = order; - this.productDescription = productDescription; - this.productName = productName; - this.price = price; - this.count = count; - } - - public void setInvoiceNumber(String invoiceNumber) { - this.invoiceNumber = invoiceNumber; - } -} - 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 6a3b07a8..00000000 --- a/src/main/java/poomasi/domain/order/entity/OrderStatus.java +++ /dev/null @@ -1,11 +0,0 @@ -package poomasi.domain.order.entity; - -public enum OrderStatus { - PENDING, // 결제 대기 중 - AWAITING_SELLER_CONFIRMATION, // 판매자 확인 대기 중 - READY_FOR_SHIPMENT, // 배송 대기 중 - IN_TRANSIT, // 배송 중 - DELIVERED, // 배송 완료 - ORDER_COMPLETE // 주문 완료 - ; -} \ 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 new file mode 100644 index 00000000..d7a6ae7d --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_abstract/AbstractOrder.java @@ -0,0 +1,72 @@ +package poomasi.domain.order.entity._abstract; + +import jakarta.persistence.*; +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.UpdateTimestamp; +import poomasi.domain.member.entity.Member; +import poomasi.domain.order._payment.entity.Payment; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + + +@MappedSuperclass +@Getter +@Setter +@SuperBuilder // 빌더 패턴을 사용하도록 설정 +@NoArgsConstructor +public abstract class AbstractOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "member_id") + @ManyToOne(fetch = FetchType.LAZY) + @Description("주문 한 사람을 참조한다.") + private Member member; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Payment payment; + + @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; + + @Column(name = "total_amount") + @Description("총 결제 금액") + private BigDecimal totalAmount; + + public void setCheckSum(BigDecimal checkSum) { + this.payment.setCheckSum(checkSum); + } + + public void subtractChecksum(BigDecimal checkSum) { + this.payment.subtractCheckSum(checkSum); + } + + public BigDecimal getCheckSum(){ + return this.payment.getCheckSum(); + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } + + +} + diff --git a/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java new file mode 100644 index 00000000..46501720 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrder.java @@ -0,0 +1,43 @@ +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 { + /* + @OneToOne(fetch=FetchType.LAZY) + private FarmOrderDetails farmOrderDetails; + + @Column(name = "owner_id") + private Long ownerId; + + @Comment("농장 간단 설명") + private String description; + + @Comment("도로명 주소") + private String destinationAddress; + + @Comment("상세 주소") + private String addressDetail; + + @Comment("위도") + private Double latitude; + + @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/_farm/FarmOrderDetails.java b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrderDetails.java new file mode 100644 index 00000000..7b6e194c --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_farm/FarmOrderDetails.java @@ -0,0 +1,28 @@ +package poomasi.domain.order.entity._farm; + +import jakarta.persistence.*; +import poomasi.domain.farm.entity.Farm; + +//@Entity +//@Table(name="farm_order_details") +public class FarmOrderDetails { + /* + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "farm_order_details_id") + private Long id; + + @OneToOne + private FarmOrder farmOrder; + + + @Column(name="farm_name") + private String farmName; + + @Column(name="farm_address") + private String farmAddress; + + +*/ + +} diff --git a/src/main/java/poomasi/domain/order/entity/_farm/OrderedFarm.java b/src/main/java/poomasi/domain/order/entity/_farm/OrderedFarm.java new file mode 100644 index 00000000..1c785e39 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_farm/OrderedFarm.java @@ -0,0 +1,20 @@ +package poomasi.domain.order.entity._farm; + + +import jakarta.persistence.*; +import poomasi.domain.farm.entity.Farm; + +@Entity +@Table(name = "ordered_farm") +public class OrderedFarm { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ordered_farm_id") + private Long id; + + @OneToOne + private Farm farm; + + +} diff --git a/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java b/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java new file mode 100644 index 00000000..13c988c0 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_product/OrderedProduct.java @@ -0,0 +1,137 @@ +package poomasi.domain.order.entity._product; + + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import poomasi.domain.order._aftersales.entity._product.ProductAfterSalesDetail; +import poomasi.domain.product.entity.Product; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +@Entity +@Table(name = "ordered_products") +@Getter +@NoArgsConstructor +public class OrderedProduct implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ordered_product_id") + private Long id; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(nullable = true, name = "product_after_sales_detail_id") + private List productAfterSalesDetails; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_order_id") + private ProductOrder productOrder; + + //FIXME : store Id를 참조해야 한다. + //나중에 store Id로 변경해야 한다 + //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; + + @Column(name = "product_name", length = 255) + private String productName; + + @Description("구매 당시 1개당 가격") + private BigDecimal price; + + @Description("구매 수량") + @Column(name="count") + private Integer count; + + @Description("송장 번호") + @Column(name = "invoice_number", nullable = true) + private String invoiceNumber; + + 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 후 + // web hook controller 만들어서 + // 배송 상태 적절히 변경해야 함 + + @Builder + public OrderedProduct(Product product, ProductOrder productOrder, String productDescription, String productName, BigDecimal price, Integer count) { + this.product = product; + this.productOrder = productOrder; + this.productDescription = productDescription; + this.productName = productName; + this.price = price; + this.count = count; + } + + public void setInvoiceNumber(String invoiceNumber) { + this.invoiceNumber = invoiceNumber; + } + + 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 new file mode 100644 index 00000000..90c237e2 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_product/ProductOrder.java @@ -0,0 +1,68 @@ +package poomasi.domain.order.entity._product; + + +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.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 +@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; + + @OneToOne + @JoinColumn(name = "product_order_details_id") // 외래 키 지정 + @Description("상품 배송지, 요청 사항") + private ProductOrderDetails productOrderDetails; + + public ProductOrder(){ + + } + + 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 new file mode 100644 index 00000000..0cf2e983 --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_product/ProductOrderDetails.java @@ -0,0 +1,47 @@ +package poomasi.domain.order.entity._product; + +import jakarta.persistence.*; +import jdk.jfr.Description; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name="product_order_details") +@Getter +@NoArgsConstructor +public class ProductOrderDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(mappedBy = "productOrderDetails", cascade = CascadeType.ALL) // 필드명으로 지정 + private ProductOrder productOrder; + + @Column(name = "return_address") + @Description("도착 주소") + private String destinationAddress; + + @Column(name = "destination_address_detail") + @Description("도착 상세 주소") + private String destinationAddressDetail; + + @Description("배송 요청 사항") + @Column(name = "delivery_request", length = 255) + private String deliveryRequest; + + + @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/ProductsOrderDetailsStatus.java b/src/main/java/poomasi/domain/order/entity/_product/ProductsOrderDetailsStatus.java new file mode 100644 index 00000000..53a53fab --- /dev/null +++ b/src/main/java/poomasi/domain/order/entity/_product/ProductsOrderDetailsStatus.java @@ -0,0 +1,11 @@ +package poomasi.domain.order.entity._product; + +public enum ProductsOrderDetailsStatus { + WAITING_SHIPPING, + IN_SHIPPING, // 배송 중 + DELIVERED_COMPLETE, // 배송 완료 + CANCELLED, // 취소 + RETURNED, // 환불 + EXCHANGED // 교환 + ; +} diff --git a/src/main/java/poomasi/domain/order/repository/OrderProductDetailsRepository.java b/src/main/java/poomasi/domain/order/repository/OrderProductDetailsRepository.java deleted file mode 100644 index 61432033..00000000 --- a/src/main/java/poomasi/domain/order/repository/OrderProductDetailsRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package poomasi.domain.order.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import poomasi.domain.order.entity.OrderProductDetails; - -import java.util.List; - -public interface OrderProductDetailsRepository extends JpaRepository { - List findByOrderId(Long orderId); -} diff --git a/src/main/java/poomasi/domain/order/repository/OrderRepository.java b/src/main/java/poomasi/domain/order/repository/OrderRepository.java deleted file mode 100644 index f2476ad0..00000000 --- a/src/main/java/poomasi/domain/order/repository/OrderRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package poomasi.domain.order.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import poomasi.domain.order.entity.Order; -import poomasi.domain.order.entity.OrderProductDetails; - -import java.util.List; -import java.util.Optional; - -public interface OrderRepository extends JpaRepository { - List findByMemberId(Long memberId); - //List findById(Long id); - Optional findByMerchantUid(String merchantUid); -} diff --git a/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java b/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java new file mode 100644 index 00000000..4827c4de --- /dev/null +++ b/src/main/java/poomasi/domain/order/repository/OrderedProductRepository.java @@ -0,0 +1,9 @@ +package poomasi.domain.order.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.order.entity._product.OrderedProduct; + +import java.util.List; + +public interface OrderedProductRepository extends JpaRepository { +} diff --git a/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java b/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java new file mode 100644 index 00000000..c439df8a --- /dev/null +++ b/src/main/java/poomasi/domain/order/repository/ProductOrderRepository.java @@ -0,0 +1,14 @@ +package poomasi.domain.order.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import poomasi.domain.order.entity._product.ProductOrder; + +import java.util.List; +import java.util.Optional; + +public interface ProductOrderRepository extends JpaRepository { + List findByMemberId(Long memberId); + Optional findByMerchantUid(String merchantUid); + //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 24175672..1f89f786 100644 --- a/src/main/java/poomasi/domain/order/service/OrderService.java +++ b/src/main/java/poomasi/domain/order/service/OrderService.java @@ -1,198 +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._payment.repository.PaymentRepository; -import poomasi.domain.order._payment.service.PaymentService; -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.Order; -import poomasi.domain.order.entity.OrderDetails; -import poomasi.domain.order.entity.OrderProductDetails; -import poomasi.domain.order.entity.OrderStatus; -import poomasi.domain.order.repository.OrderProductDetailsRepository; -import poomasi.domain.order.repository.OrderRepository; -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.domain.order.entity.OrderStatus.AWAITING_SELLER_CONFIRMATION; -import static poomasi.global.error.BusinessError.*; - -@RequiredArgsConstructor -@Service -@Slf4j -public class OrderService { - - private final OrderRepository orderRepository; - private final CartRepository cartRepository; - private final ProductRepository productRepository; - private final PaymentRepository paymentRepository; - private final PaymentService paymentService; - private final OrderProductDetailsRepository orderProductDetailsRepository; - - @Transactional - public PaymentPreRegisterRequest preOrderRegister(){ - Member member = getMember(); - Long memberId = member.getId(); - List cartList = cartRepository.findByMemberIdAndSelected(memberId); - - //TODO : dto에서 address, address detail 꺼내와야 함. -> dto 확정이 안 나서 임시로 넣음 - String address = "금정구"; - String addressDetails = "수림로"; - String deliveryRequest = "조심히 다뤄 주세요"; - - Order order = new Order( - new OrderDetails(address, - addressDetails, - deliveryRequest) - ); - - //cart에 있는 총 가격 계산하기 - BigDecimal totalPrice = BigDecimal.ZERO; - - // cart 돌면서 order details 추가 - for (Cart cart : cartList) { - Long productId = cart.getProductId(); - Product product = productRepository.findById(productId) - .orElseThrow(() -> new BusinessException(PRODUCT_NOT_FOUND)); - String productDescription = product.getDescription(); - Integer count = cart.getCount(); - String productName = product.getName(); - BigDecimal price = BigDecimal.valueOf(product.getPrice()); - OrderProductDetails orderProductDetails = OrderProductDetails - .builder() - .product(product) - .order(order) - .productDescription(productDescription) - .productName(productName) - .price(price) - .count(count) - .build(); - order.addOrderDetail(orderProductDetails); - totalPrice = totalPrice.add(price); - } - order.setTotalAmount(totalPrice); - orderRepository.save(order); - - String merchantUid = order.getMerchantUid(); - return new PaymentPreRegisterRequest(merchantUid, totalPrice); - } - - @Description("멤버 ID 기반으로 모든 order 다 들고 오는 메서드") - public List findAllOrdersByMemberId(){ - Member member = getMember(); - Long memberId = member.getId(); - List orderList = orderRepository.findByMemberId(memberId); - return orderList - .stream() - .map(OrderResponse::fromEntity) - .collect(Collectors.toList() - ); - } - - @Description("멤버 id 기반으로 특정 orderId 들고오는 메서드") - public OrderResponse findOrderByMemberId(Long orderId){ - Member member = getMember(); - Order order = orderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - - validateOrderOwnership(order, member); - return OrderResponse.fromEntity(order); - } - - - @Description("orderId 기반으로 order details(주소, 상세주소, 배송 요청 사항 ..등) 들고오는 메서드") - public OrderDetailsResponse findOrderDetailsByOrderId(Long orderId){ - Order order = orderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - OrderDetails orderDetails = order.getOrderDetails(); - - return OrderDetailsResponse.fromEntity(orderDetails); - } - - @Description("orderId에 해당하는 order product details 가져오는 메서드") - public List findAllOrderProductDetails(Long orderId){ - Member member = getMember(); - Order order = orderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - validateOrderOwnership(order, member); - return order.getOrderProductDetails() - .stream() - .map(OrderProductDetailsResponse::fromEntity) - .collect(Collectors.toList() - ); - } - - - @Description("orderId에 해당하는 order product Details의 단건 조회") - public OrderProductDetailsResponse findOrderProductDetailsById(Long orderId, Long orderProductDetailsId){ - Member member = getMember(); - OrderProductDetails orderProductDetails = orderProductDetailsRepository.findById(orderProductDetailsId) - .orElseThrow(()-> new BusinessException(ORDER_PRODUCT_DETAILS_NOT_FOUND)); - Order order = orderProductDetails.getOrder(); - - // order product details의 주인 order 검사 그리고 , orderId의 주인 member 검사 - validateOrderProductDetailsByOrderId(order, orderId); - validateOrderOwnership(order, member); - - return OrderProductDetailsResponse.fromEntity(orderProductDetails); - } - - - @Description("member의 order인지 검사하는 메서드") - private void validateOrderOwnership(Order order, Member member) { - if (!order.getMember().getId().equals(member.getId())) { - throw new BusinessException(ORDER_NOT_OWNED_EXCEPTION); - } - } +public interface OrderService { - @Description("orderId에 해당하는 order Product Details인지 조회하는 메서드") - private void validateOrderProductDetailsByOrderId(Order order, Long orderId) { - if(order.getId()!=orderId){ - throw new BusinessException(ORDER_PRODUCT_DETAILS_NOT_OWNED_EXCEPTION); - } - } - - @Description("결제가 완료 된 후, 주문 상태 변경하는 메서드. 굳이 없어도 되긴 함.") - private void completePaymentAndUpdateStatus(Long orderId){ - Order order = orderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - order.setOrderStatus(AWAITING_SELLER_CONFIRMATION); - } - - @Description("주문 상태를 변경하는 메서드") - private void changeOrderStatus(Long orderId, OrderStatus orderStatus){ - Order order = orderRepository.findById(orderId) - .orElseThrow(()-> new BusinessException(ORDER_NOT_FOUND)); - order.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/domain/product/entity/Product.java b/src/main/java/poomasi/domain/product/entity/Product.java index 8c70ba51..cd80bfbc 100644 --- a/src/main/java/poomasi/domain/product/entity/Product.java +++ b/src/main/java/poomasi/domain/product/entity/Product.java @@ -1,19 +1,7 @@ package poomasi.domain.product.entity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; +import jakarta.persistence.*; + import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -23,7 +11,7 @@ import org.hibernate.annotations.Comment; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; -import poomasi.domain.order.entity.OrderProductDetails; +import poomasi.domain.order.entity._product.OrderedProduct; import poomasi.domain.store.entity.Store; import poomasi.domain.product.dto.ProductRegisterRequest; import poomasi.domain.review.entity.Review; @@ -87,7 +75,7 @@ public class Product { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "order_product_details_id") - private List orderProductDetails; + private List orderProductDetails; @Builder @@ -125,4 +113,17 @@ public void addStock(Integer stock) { this.stock += stock; } + public void addReview(Review pReview) { + this.reviewList.add(pReview); + this.averageRating = reviewList.stream() + .mapToDouble(Review::getRating) // 각 리뷰의 평점을 double로 변환 + .average() // 평균 계산 + .orElse(0.0); + } + + public void subtractStock(Integer stock) { + this.stock -= stock; + } + + } diff --git a/src/main/java/poomasi/global/error/BusinessError.java b/src/main/java/poomasi/global/error/BusinessError.java index 67d668d4..6e5cfa81 100644 --- a/src/main/java/poomasi/global/error/BusinessError.java +++ b/src/main/java/poomasi/global/error/BusinessError.java @@ -1,5 +1,7 @@ package poomasi.global.error; +import org.springframework.http.HttpStatus; + import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -10,6 +12,7 @@ public enum BusinessError { // Product PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다."), PRODUCT_STOCK_ZERO(HttpStatus.BAD_REQUEST, "재고가 없습니다."), + STOCK_QUANTITY_EXCEEDED(HttpStatus.BAD_REQUEST, "장바구나 수량이 남은 재고를 초과하였습니다"), // Category CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다."), @@ -81,15 +84,32 @@ 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; private final String message; } + diff --git a/src/main/java/poomasi/global/error/ExceptionAdvice.java b/src/main/java/poomasi/global/error/ExceptionAdvice.java index 5e5e536b..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 { @@ -30,4 +34,35 @@ public ErrorResponse applicationExceptionHandler(ApplicationException exception) .title(exception.getClass().getSimpleName()) .build(); } + + @ExceptionHandler(PaymentConfirmException.class) + public ErrorResponse paymentConfirmExceptionHandler(PaymentConfirmException exception) { + PaymentConfirmError paymentConfirmError = exception.getPaymentConfirmError(); + log.error("[{}] : {}", paymentConfirmError.name(), paymentConfirmError.getReason()); + return ErrorResponse + .builder(exception, paymentConfirmError.getHttpStatus(), paymentConfirmError.getReason()) + .title(paymentConfirmError.name()) + .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(); + } + + } diff --git a/src/main/java/poomasi/global/error/PaymentConfirmError.java b/src/main/java/poomasi/global/error/PaymentConfirmError.java new file mode 100644 index 00000000..a04ddafb --- /dev/null +++ b/src/main/java/poomasi/global/error/PaymentConfirmError.java @@ -0,0 +1,19 @@ +package poomasi.global.error; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum PaymentConfirmError { + + PAYMENT_PROUCT_CONFIRM_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "결제 직전 상품 수량 부족") + ; + + private final HttpStatus httpStatus; + private final String reason; + + +} diff --git a/src/main/java/poomasi/global/error/PaymentConfirmException.java b/src/main/java/poomasi/global/error/PaymentConfirmException.java new file mode 100644 index 00000000..ac03ed78 --- /dev/null +++ b/src/main/java/poomasi/global/error/PaymentConfirmException.java @@ -0,0 +1,12 @@ +package poomasi.global.error; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentConfirmException extends RuntimeException { + + private final PaymentConfirmError paymentConfirmError; + +}