diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/dto/CancelOrderRequest.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/dto/CancelOrderRequest.java new file mode 100644 index 00000000..76c6e86e --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/dto/CancelOrderRequest.java @@ -0,0 +1,5 @@ +package com.nowait.applicationadmin.cancelOrder.dto; + +import com.nowait.domainadminrdb.cancelOrder.entity.CancelReason; + +public record CancelOrderRequest(CancelReason reason) { } diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/dto/CancelOrderResponse.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/dto/CancelOrderResponse.java new file mode 100644 index 00000000..7ee7a2f8 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/dto/CancelOrderResponse.java @@ -0,0 +1,29 @@ +package com.nowait.applicationadmin.cancelOrder.dto; + +import java.time.Instant; + +import com.nowait.domainadminrdb.cancelOrder.entity.CancelOrder; +import com.nowait.domainadminrdb.cancelOrder.entity.CancelReason; + +import lombok.AllArgsConstructor; +import lombok.Builder; + +@Builder +public class CancelOrderResponse { + + private Long id; + private Long orderId; + private String orderSignature; + private CancelReason reason; + private Instant cancelAt; + + public CancelOrder fromEntity() { + return CancelOrder.builder() + .id(id) + .orderId(orderId) + .orderSignature(orderSignature) + .reason(reason) + .cancelAt(cancelAt) + .build(); + } +} diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/listener/OrderCancelledEventListener.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/listener/OrderCancelledEventListener.java new file mode 100644 index 00000000..c6fcbd39 --- /dev/null +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/cancelOrder/listener/OrderCancelledEventListener.java @@ -0,0 +1,41 @@ +package com.nowait.applicationadmin.cancelOrder.listener; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import com.nowait.domainadminrdb.cancelOrder.entity.CancelOrder; +import com.nowait.domainadminrdb.cancelOrder.repository.CancelOrderRepository; +import com.nowait.nowaitevent.order.event.OrderCancelledEvent; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class OrderCancelledEventListener { + + private final CancelOrderRepository cancelOrderRepository; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void on(OrderCancelledEvent event) { + try { + cancelOrderRepository.save( + CancelOrder.builder() + .orderId(event.getOrderId()) + .storeId(event.getStoreId()) + .orderSignature(event.getOrderSignature()) + .reason(event.getReason()) + .cancelAt(event.getCancelAt()) + .build() + ); + } catch (DataIntegrityViolationException e) { + log.debug("cancel_orders duplicate orderId={}, ignore", event.getOrderId()); + } + } +} diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java index 3e7e7efa..5a7ea897 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/controller/OrderController.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.nowait.applicationadmin.cancelOrder.dto.CancelOrderRequest; import com.nowait.applicationadmin.order.dto.OrderResponseDto; import com.nowait.applicationadmin.order.dto.OrderStatusUpdateRequestDto; import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; @@ -63,4 +65,20 @@ public ResponseEntity updateOrderStatus( .status(HttpStatus.OK) .body(ApiUtils.success(response)); } + + @DeleteMapping("/{orderId}") + @Operation(summary = "주문 삭제", description = "특정 주문을 삭제") + @ApiResponse(responseCode = "200", description = "주문 삭제 성공") + public ResponseEntity deleteOrder( + @PathVariable Long orderId, + @RequestBody CancelOrderRequest cancelOrderRequest, + @AuthenticationPrincipal MemberDetails memberDetails + ) { + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiUtils.success( + orderService.cancelOrder(orderId, cancelOrderRequest, memberDetails) + )); + } } diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java index dfc88532..cfd26222 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java @@ -1,5 +1,6 @@ package com.nowait.applicationadmin.order.service; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; @@ -10,6 +11,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.nowait.applicationadmin.cancelOrder.dto.CancelOrderRequest; import com.nowait.applicationadmin.order.dto.OrderResponseDto; import com.nowait.applicationadmin.order.dto.OrderStatusUpdateResponseDto; import com.nowait.common.enums.Role; @@ -29,6 +31,7 @@ import com.nowait.domaincorerdb.user.exception.UserNotFoundException; import com.nowait.domaincorerdb.user.repository.UserRepository; import com.nowait.nowaitevent.order.event.CookingCompleteEvent; +import com.nowait.nowaitevent.order.event.OrderCancelledEvent; import lombok.RequiredArgsConstructor; @@ -87,6 +90,29 @@ public OrderStatusUpdateResponseDto updateOrderStatus(Long orderId, OrderStatus return OrderStatusUpdateResponseDto.fromEntity(userOrder); } + @Transactional + public OrderStatusUpdateResponseDto cancelOrder(Long orderId, CancelOrderRequest cancelOrderRequest, MemberDetails memberDetails) { + User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new); + UserOrder userOrder = orderRepository.findById(orderId).orElseThrow(OrderNotFoundException::new); + + if (!Role.SUPER_ADMIN.equals(user.getRole()) && !user.getStoreId().equals(userOrder.getStore().getStoreId())) { + throw new OrderUpdateUnauthorizedException(); + } + + userOrder.cancelOrder(); + + publisher.publishEvent(new OrderCancelledEvent( + userOrder.getId(), + userOrder.getStore().getStoreId(), + userOrder.getSignature(), + cancelOrderRequest.reason(), + Instant.now() + )); + + + return OrderStatusUpdateResponseDto.fromEntity(userOrder); + } + @Transactional(readOnly = true) public OrderSalesSumDetail getSaleSumByStoreId(MemberDetails memberDetails, LocalDate date) { User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new); diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/entity/CancelOrder.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/entity/CancelOrder.java new file mode 100644 index 00000000..7ee01640 --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/entity/CancelOrder.java @@ -0,0 +1,52 @@ +package com.nowait.domainadminrdb.cancelOrder.entity; + +import java.time.Instant; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "cancel_orders") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class CancelOrder { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long orderId; + + @Column(name = "store_id", nullable = false) + private Long storeId; + + @Column(nullable = false, length = 256) + private String orderSignature; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private CancelReason reason; + + @Column(nullable = false) + private Instant cancelAt; + + @PrePersist + void prePersist() { + if (cancelAt == null) cancelAt = Instant.now(); + } +} diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/entity/CancelReason.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/entity/CancelReason.java new file mode 100644 index 00000000..66245b32 --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/entity/CancelReason.java @@ -0,0 +1,21 @@ +package com.nowait.domainadminrdb.cancelOrder.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(description = "주문 상태 Enum") +public enum CancelReason { + + @Schema(description = "단순 취소") + SIMPLE_CANCEL("단순 취소"), + @Schema(description = "메뉴 품절") + SOLD_OUT("메뉴 품절"), + @Schema(description = "기타") + ETC("기타"); + + private final String description; +} + diff --git a/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/repository/CancelOrderRepository.java b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/repository/CancelOrderRepository.java new file mode 100644 index 00000000..790a9a3a --- /dev/null +++ b/nowait-domain/domain-admin-rdb/src/main/java/com/nowait/domainadminrdb/cancelOrder/repository/CancelOrderRepository.java @@ -0,0 +1,10 @@ +package com.nowait.domainadminrdb.cancelOrder.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.nowait.domainadminrdb.cancelOrder.entity.CancelOrder; + +@Repository +public interface CancelOrderRepository extends JpaRepository { +} diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java index 322a5218..7e022a5b 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/OrderStatus.java @@ -16,7 +16,10 @@ public enum OrderStatus { COOKING("조리중"), @Schema(description = "조리완료") - COOKED("조리완료"); + COOKED("조리완료"), + + @Schema(description = "주문취소") + CANCELLED("주문취소"); private final String description; } diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java index 5dbd0295..54fd36a9 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java @@ -30,6 +30,7 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder public class UserOrder extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -49,6 +50,7 @@ public class UserOrder extends BaseTimeEntity { private List orderItems = new ArrayList<>(); private String sessionId; + @Column(length = 10) // 예약자 이름 길이 제한 private String depositorName; @@ -63,4 +65,11 @@ public void updateStatus(OrderStatus newStatus) { this.status = newStatus; } + public void cancelOrder() { + if (this.status == OrderStatus.CANCELLED) { + return; + } + this.status = OrderStatus.CANCELLED; + } + } diff --git a/nowait-event/build.gradle b/nowait-event/build.gradle index 067d51c3..f802a2ff 100644 --- a/nowait-event/build.gradle +++ b/nowait-event/build.gradle @@ -25,6 +25,7 @@ repositories { dependencies { implementation project(':nowait-common') implementation project(':nowait-domain:domain-core-rdb') + implementation project(':nowait-domain:domain-admin-rdb') api 'org.springframework.boot:spring-boot-starter-data-jpa' api 'jakarta.persistence:jakarta.persistence-api:3.1.0' diff --git a/nowait-event/src/main/java/com/nowait/nowaitevent/order/event/OrderCancelledEvent.java b/nowait-event/src/main/java/com/nowait/nowaitevent/order/event/OrderCancelledEvent.java new file mode 100644 index 00000000..117655e6 --- /dev/null +++ b/nowait-event/src/main/java/com/nowait/nowaitevent/order/event/OrderCancelledEvent.java @@ -0,0 +1,16 @@ +package com.nowait.nowaitevent.order.event; + +import java.time.Instant; + +import com.nowait.domainadminrdb.cancelOrder.entity.CancelReason; + +import lombok.Value; + +@Value +public class OrderCancelledEvent { + Long orderId; + Long storeId; + String orderSignature; + CancelReason reason; + Instant cancelAt; +}