Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

step3 (매장 식사 주문) #246

Open
wants to merge 7 commits into
base: yuhwanwoo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 41 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ docker compose -p kitchenpos up -d

| 한글명 | 영문명 | 설명 |
| --- | --- | --- |
| 매장 주문 | eat in order | 매장에서 안에서의 주문 |
| 방문한 손님 수 | number of guests | 식기가 필요한 사람 수. 필수 사항은 아니며 주문은 0명으로 등록할 수 있다. |
| 빈 테이블 | empty table | 주문을 등록할 수 없는 주문 테이블 |
| 서빙 | served | 조리가 완료되어 음식이 나갈 수 있는 단계 |
Expand All @@ -134,18 +135,19 @@ docker compose -p kitchenpos up -d

### 배달 주문

| 한글명 | 영문명 | 설명 |
| --- | --- | --- |
| 배달 | delivering | 배달원이 매장을 방문하여 배달 음식의 픽업을 완료하고 배달을 시작하는 단계 |
| 배달 대행사 | delivery agency | 준비한 음식을 고객에게 직접 배달하는 서비스 |
| 배달 완료 | delivered | 배달원이 주문한 음식을 고객에게 배달 완료한 단계 |
| 서빙 | served | 조리가 완료되어 음식이 나갈 수 있는 단계 |
| 완료 | completed | 배달 및 결제 완료 단계 |
| 접수 | accepted | 주문을 받고 음식을 조리하는 단계 |
| 접수 대기 | waiting | 주문이 생성되어 매장으로 전달된 단계 |
| 주문 | order | 집이나 직장 등 고객이 선택한 주소로 음식을 배달한다. |
| 주문 상태 | order status | 주문이 생성되면 매장에서 주문을 접수하고 고객이 음식을 받기까지의 단계를 표시한다. |
| 주문 항목 | order line item | 주문에 속하는 수량이 있는 메뉴 |
| 한글명 | 영문명 | 설명 |
|--------|----------------|------------------------------------------------|
| 배달 주문 | delivery order | 배달을 위한 주문 |
| 배달 | delivering | 배달원이 매장을 방문하여 배달 음식의 픽업을 완료하고 배달을 시작하는 단계 |
| 배달 대행사 | delivery agency | 준비한 음식을 고객에게 직접 배달하는 서비스 |
| 배달 완료 | delivered | 배달원이 주문한 음식을 고객에게 배달 완료한 단계 |
| 서빙 | served | 조리가 완료되어 음식이 나갈 수 있는 단계 |
| 완료 | completed | 배달이 완료된 후 주문을 마무리 하는 단계 |
| 접수 | accepted | 주문을 받고 음식을 조리하는 단계 |
| 접수 대기 | waiting | 주문이 생성되어 매장으로 전달된 단계 |
| 주문 | order | 집이나 직장 등 고객이 선택한 주소로 음식을 배달한다. |
| 주문 상태 | order status | 주문이 생성되면 매장에서 주문을 접수하고 고객이 음식을 받기까지의 단계를 표시한다. |
| 주문 항목 | order line item | 주문에 속하는 수량이 있는 메뉴 |

### 포장 주문

Expand Down Expand Up @@ -181,27 +183,42 @@ docker compose -p kitchenpos up -d
### 매장 주문

- `OrderTable`은 식별자와 이름, `NumberOfGuests`를 가진다.
- `OrderTable`의 추가 `Order`는 `OrderTable`에 계속 쌓이며 모든 `Order`가 완료되면 `EmptyTable`이 된다.
- `OrderTable`의 추가 `Order`는 `OrderTable`에 계속 쌓이며 모든 `EatInOrder`가 완료되면 `EmptyTable`이 된다.
- `EmptyTable`인 경우 `NumberOfGuests`는 0이며 변경할 수 없다.
- `Order`는 식별자와 `OrderStatus`, 주문 시간, `OrderLineItems`를 가진다.
- 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Order`가 생성된다.
- `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다.
- `EatInOrder`는 식별자와 `OrderStatus`, 주문 시간, `OrderLineItems`를 가진다.
- `Menu`가 `Display Menu`되고 있으며 판매되는 메뉴 가격과 일치하면 `EatInOrder`가 생성된다.
- `EatInOrder`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다.
- `EatInOrder`가 생성되면 `OrderStatus`는 `Waiting` 상태이다.
- `Accepted` 가 되려면 전 주문은 `Waiting` 상태여야 한다.
- `Served` 가 되려면 전 주문은 `Accepted` 상태여야 한다.
- `Completed` 가 되려면 전 주문은 `Served` 상태여야 한다.
Comment on lines +188 to +194
Copy link

Choose a reason for hiding this comment

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

모델링 업데이트 👍 👍
전략적 설계를 하면서 구성하신 모델링과 실제 구현한 코드간의 싱크를 맞춰주시는 것에 대해서도 꾸준히 유지보수가 필요하다고 생각해요
모델링과 구현코드간의 싱크가 맞지 않으면, 멘탈모델이 일치하지 않아 결국 DDD 도입 전과 같은 문제들이 계속 발생하게 되는 이슈가 있어요 😢

- `OrderLineItem`는 가격과 수량을 가진다.
- `OrderLineItem`의 수량은 기존 `Order`를 취소하거나 변경해도 수정되지 않기 때문에 0보다 적을 수 있다.
- `OrderLineItem`의 수량은 기존 `EatInOrder`를 취소하거나 변경해도 수정되지 않기 때문에 0보다 적을 수 있다.
- ``

### 배달 주문

- `Order`는 식별자와 `OrderStatus`, 주문 시간, 배달 주소, `OrderLineItems`를 가진다.
- 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Order`가 생성된다.
- `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 배달 ➜ 배달 완료 ➜ 계산 완료 순서로 진행된다.
- `Order`가 접수되면 `DeliveryAgency`가 호출된다.
- `Delivery Order`는 식별자와 `OrderStatus`, 주문 시간, 배달 주소, `OrderLineItems`를 가진다.
- 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Delivery Order`가 생성된다.
- `Delivery Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 배달 ➜ 배달 완료 ➜ 계산 완료 순서로 진행된다.
- `Delivery Order`가 생성되면 `OrderStatus`는 `Waiting` 상태이다.
- `Delivery Order`가 접수되면 `DeliveryAgency`가 호출된다.
- `Accepted` 가 되려면 전 주문은 `Waiting` 상태여야 한다.
- `Served` 가 되려면 전 주문은 `Accepted` 상태여야 한다.
- `Delivering` 가 되려면 전 주문은 `Served` 상태여야 한다.
- `Delivered` 가 되려면 전 주문은 `Delivering` 상태여야 한다.
- `Completed` 가 되려면 전 주문은 `Delivered` 상태여야 한다.
- `OrderLineItem`는 가격과 수량을 가진다.
- `OrderLineItem`의 수량은 1보다 커야 한다.

### 포장 주문

- `Order`는 식별자와 `OrderStatus`, 주문 시간, `OrderLineItems`를 가진다.
- 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Order`가 생성된다.
- `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다.
- `Take Out Order`는 식별자와 `OrderStatus`, 주문 시간, `OrderLineItems`를 가진다.
- 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Take Out Order`가 생성된다.
- `Take Out Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다.
- `Take Out Order`가 생성되면 `OrderStatus`는 `Waiting` 상태이다.
- `Accepted` 가 되려면 전 주문은 `Waiting` 상태여야 한다.
- `Served` 가 되려면 전 주문은 `Accepted` 상태여야 한다.
- `Completed` 가 되려면 전 주문은 `Served` 상태여야 한다.
- `OrderLineItem`는 가격과 수량을 가진다.
- `OrderLineItem`의 수량은 1보다 커야 한다.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package kitchenpos.deliveryorders.application;

import kitchenpos.deliveryorders.domain.DeliveryOrder;
import kitchenpos.deliveryorders.domain.DeliveryOrderAddress;
import kitchenpos.deliveryorders.domain.DeliveryOrderLineItem;
import kitchenpos.deliveryorders.domain.DeliveryOrderLineItemPrice;
import kitchenpos.deliveryorders.domain.DeliveryOrderLineItemQuantity;
import kitchenpos.deliveryorders.domain.DeliveryOrderLineItems;
import kitchenpos.deliveryorders.domain.DeliveryOrderRepository;
import kitchenpos.deliveryorders.domain.KitchenridersClient;
import kitchenpos.deliveryorders.domain.MenuClient;
import kitchenpos.deliveryorders.shared.dto.DeliveryOrderLineItemDto;
import kitchenpos.deliveryorders.shared.dto.DeliveryOrderDto;
import kitchenpos.deliveryorders.shared.dto.request.DeliveryOrderCreateRequest;
import kitchenpos.shared.util.ConvertUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
public class DeliveryOrderService {
private DeliveryOrderRepository deliveryOrderRepository;
private MenuClient menuClient;
private KitchenridersClient kitchenridersClient;

public DeliveryOrderService(DeliveryOrderRepository deliveryOrderRepository, MenuClient menuClient, KitchenridersClient kitchenridersClient) {
this.deliveryOrderRepository = deliveryOrderRepository;
this.menuClient = menuClient;
this.kitchenridersClient = kitchenridersClient;
}

@Transactional
public DeliveryOrderDto create(final DeliveryOrderCreateRequest request) {
final List<DeliveryOrderLineItemDto> orderLineItemRequests = request.getOrderLineItems();

if (Objects.isNull(orderLineItemRequests) || orderLineItemRequests.isEmpty()) {
throw new IllegalArgumentException();
}

List<DeliveryOrderLineItem> deliveryOrderLineItems = request.getOrderLineItems().stream()
.map(deliveryOrderLineItemDto -> DeliveryOrderLineItem.of(
deliveryOrderLineItemDto.getMenuId(),
new DeliveryOrderLineItemQuantity(deliveryOrderLineItemDto.getQuantity()),
new DeliveryOrderLineItemPrice(deliveryOrderLineItemDto.getPrice()),
menuClient
))
.collect(Collectors.toList());

DeliveryOrder deliveryOrder = DeliveryOrder.of(
new DeliveryOrderLineItems(deliveryOrderLineItems),
new DeliveryOrderAddress(request.getDeliveryAddress()),
menuClient
);

return ConvertUtil.convert(deliveryOrderRepository.save(deliveryOrder), DeliveryOrderDto.class);
}

@Transactional
public DeliveryOrderDto accept(final UUID orderId) {
DeliveryOrder deliveryOrder = deliveryOrderRepository.findById(orderId)
.orElseThrow(NoSuchElementException::new);
deliveryOrder.accept(kitchenridersClient);
return ConvertUtil.convert(deliveryOrder, DeliveryOrderDto.class);
}

@Transactional
public DeliveryOrderDto serve(final UUID orderId) {
DeliveryOrder deliveryOrder = deliveryOrderRepository.findById(orderId)
.orElseThrow(NoSuchElementException::new);
deliveryOrder.serve();
return ConvertUtil.convert(deliveryOrder, DeliveryOrderDto.class);
}

@Transactional
public DeliveryOrderDto startDelivery(final UUID orderId) {
DeliveryOrder deliveryOrder = deliveryOrderRepository.findById(orderId)
.orElseThrow(NoSuchElementException::new);
deliveryOrder.startDelivery();
return ConvertUtil.convert(deliveryOrder, DeliveryOrderDto.class);
}

@Transactional
public DeliveryOrderDto completeDelivery(final UUID orderId) {
DeliveryOrder deliveryOrder = deliveryOrderRepository.findById(orderId)
.orElseThrow(NoSuchElementException::new);
deliveryOrder.completeDelivery();
return ConvertUtil.convert(deliveryOrder, DeliveryOrderDto.class);
}

@Transactional
public DeliveryOrderDto complete(final UUID orderId) {
DeliveryOrder deliveryOrder = deliveryOrderRepository.findById(orderId)
.orElseThrow(NoSuchElementException::new);
deliveryOrder.complete();
return ConvertUtil.convert(deliveryOrder, DeliveryOrderDto.class);
}

@Transactional(readOnly = true)
public List<DeliveryOrderDto> findAll() {
return ConvertUtil.convertList(deliveryOrderRepository.findAll(), DeliveryOrderDto.class);
}
}
120 changes: 120 additions & 0 deletions src/main/java/kitchenpos/deliveryorders/domain/DeliveryOrder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package kitchenpos.deliveryorders.domain;

import kitchenpos.eatinorders.domain.OrderStatus;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Table;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@Table(name = "delivery_orders")
@Entity
public class DeliveryOrder {
@Column(name = "id", columnDefinition = "binary(16)")
@Id
private UUID id;

@Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING)
private OrderStatus status;

@Column(name = "order_date_time", nullable = false)
private LocalDateTime orderDateTime;

@Embedded
private DeliveryOrderLineItems orderLineItems;

@Embedded
private DeliveryOrderAddress deliveryAddress;

protected DeliveryOrder() {
}

public DeliveryOrder(UUID id, OrderStatus status, LocalDateTime orderDateTime, DeliveryOrderLineItems orderLineItems, DeliveryOrderAddress deliveryAddress) {
Copy link

Choose a reason for hiding this comment

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

정적 팩토리메서드를 사용하게 되면 외부에서 생성자를 통해 생성하지 못하도록 수정해보는 것은 어떤가요?
배달 주문은 생성하기 위한 로직들이 정적메서드에 구현되어 있어, 정책에 위배되는 배달주문 생성하는 위험이 존재할 것 같아요

Copy link
Author

Choose a reason for hiding this comment

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

이 부분에 대해 고민을 했던 부분이
처음 주문을 생성하면 WAITING이라는 조건 때문에 정적 팩토리메소드로 생성 시 무조건 WAITING 상태로 DeliveryOrder를 생성 하는데요,
이렇게 되면 테스트 코드 작성 시 다른 상태값을 생성하지 못해 public으로 두었습니다.
정적 팩토리 메소드를 통해서 생성시에도 상태를 받도록 변경하는 하고 테스트 코드도 of를 통해 생성하는 쪽이 좋을까요?

Copy link

Choose a reason for hiding this comment

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

저도 많은 고민을 했었는데요~ 테스트코드를 작성하기 위해 프로덕션 코드에 손이 가는 것이 괜찮을까? 고민이 들기는 했어요.

정적 팩토리 메소드를 통해서 생성시에도 상태를 받도록 변경하는 하고 테스트 코드도 of를 통해 생성하는 쪽이 좋을까요?

말씀주신 방법으로 구현해도 좋아요. 다만, 프로덕션 코드에서는 배달주문을 생성할 때 Status 값이 무조건 WAITING 이라는 점때문에 찜찜하실수 있을 것 같아요
이런경우면 of 정적팩토리메서드 대신 ReflectionTestUtils 을 이용하는 방법도 있고, Fixture 를 이용하는 방법도 있을 것 같아요
위의 2가지 방법도 같이 찾아보시면서 고려해보시면 좋을 것 같아요 😄

this.id = id;
this.status = status;
this.orderDateTime = orderDateTime;
this.orderLineItems = orderLineItems;
this.deliveryAddress = deliveryAddress;
}

public static DeliveryOrder of(DeliveryOrderLineItems orderLineItems, DeliveryOrderAddress orderAddress, MenuClient menuClient) {
validateDeliveryOrder(orderLineItems, menuClient);
return new DeliveryOrder(UUID.randomUUID(), OrderStatus.WAITING, LocalDateTime.now(), orderLineItems, orderAddress);
}

private static void validateDeliveryOrder(DeliveryOrderLineItems orderLineItems, MenuClient menuClient) {
int menusSize = menuClient.countAllByIdIn(
orderLineItems.getOrderLineItems().stream()
.map(DeliveryOrderLineItem::getMenuId)
.collect(Collectors.toList())
);
if (menusSize != orderLineItems.getOrderLineItems().size()) {
throw new IllegalArgumentException();
}
}

public UUID getId() {
return id;
}

public OrderStatus getStatus() {
return status;
}

public LocalDateTime getOrderDateTime() {
return orderDateTime;
}

public List<DeliveryOrderLineItem> getOrderLineItems() {
return orderLineItems.getOrderLineItems();
}

public String getDeliveryAddress() {
return deliveryAddress.getDeliveryOrderAddress();
}

public void accept(KitchenridersClient kitchenridersClient) {
if (status != OrderStatus.WAITING) {
throw new IllegalStateException();
}

kitchenridersClient.requestDelivery(id, orderLineItems.getTotalDeliveryOrderLineItemsPrice(), getDeliveryAddress());
status = OrderStatus.ACCEPTED;
}

public void serve() {
if (status != OrderStatus.ACCEPTED) {
throw new IllegalStateException();
}
this.status = OrderStatus.SERVED;
}

public void startDelivery() {
if (status != OrderStatus.SERVED) {
throw new IllegalStateException();
}
status = OrderStatus.DELIVERING;
}

public void completeDelivery() {
if (status != OrderStatus.DELIVERING) {
throw new IllegalStateException();
}
status = OrderStatus.DELIVERED;
}

public void complete() {
if (status != OrderStatus.DELIVERED) {
throw new IllegalStateException();
}
status = OrderStatus.COMPLETED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kitchenpos.deliveryorders.domain;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.util.Objects;

@Embeddable
public class DeliveryOrderAddress {
@Column(name = "address", nullable = false)
private String deliveryOrderAddress;

protected DeliveryOrderAddress() {
}

public DeliveryOrderAddress(String deliveryOrderAddress) {
validateDeliveryOrderAddress(deliveryOrderAddress);
this.deliveryOrderAddress = deliveryOrderAddress;
}

private void validateDeliveryOrderAddress(String deliveryOrderAddress) {
if (Objects.isNull(deliveryOrderAddress) || deliveryOrderAddress.isEmpty()) {
throw new IllegalArgumentException();
}
}

public String getDeliveryOrderAddress() {
return deliveryOrderAddress;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DeliveryOrderAddress that = (DeliveryOrderAddress) o;
return Objects.equals(deliveryOrderAddress, that.deliveryOrderAddress);
}

@Override
public int hashCode() {
return Objects.hash(deliveryOrderAddress);
}
}
Loading