├── .gitignore ├── README.md ├── build.gradle.kts ├── docker └── docker-compose.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── http ├── menu-groups.http ├── menus.http ├── order-tables.http ├── orders.http └── products.http ├── rest-client.env.json ├── settings.gradle.kts └── src ├── main ├── java │ └── kitchenpos │ │ ├── Application.java │ │ ├── deliveryorders │ │ └── infra │ │ │ ├── DefaultKitchenridersClient.java │ │ │ └── KitchenridersClient.java │ │ ├── eatinorders │ │ ├── application │ │ │ ├── OrderService.java │ │ │ └── OrderTableService.java │ │ ├── domain │ │ │ ├── JpaOrderRepository.java │ │ │ ├── JpaOrderTableRepository.java │ │ │ ├── Order.java │ │ │ ├── OrderLineItem.java │ │ │ ├── OrderRepository.java │ │ │ ├── OrderStatus.java │ │ │ ├── OrderTable.java │ │ │ ├── OrderTableRepository.java │ │ │ └── OrderType.java │ │ └── ui │ │ │ ├── OrderRestController.java │ │ │ └── OrderTableRestController.java │ │ ├── menus │ │ ├── application │ │ │ ├── MenuGroupService.java │ │ │ └── MenuService.java │ │ ├── domain │ │ │ ├── JpaMenuGroupRepository.java │ │ │ ├── JpaMenuRepository.java │ │ │ ├── Menu.java │ │ │ ├── MenuGroup.java │ │ │ ├── MenuGroupRepository.java │ │ │ ├── MenuProduct.java │ │ │ └── MenuRepository.java │ │ └── ui │ │ │ ├── MenuGroupRestController.java │ │ │ └── MenuRestController.java │ │ ├── products │ │ ├── application │ │ │ └── ProductService.java │ │ ├── domain │ │ │ ├── JpaProductRepository.java │ │ │ ├── Product.java │ │ │ └── ProductRepository.java │ │ ├── infra │ │ │ ├── DefaultPurgomalumClient.java │ │ │ └── PurgomalumClient.java │ │ └── ui │ │ │ └── ProductRestController.java │ │ └── takeoutorders │ │ └── empty.txt └── resources │ ├── application.properties │ ├── db │ └── migration │ │ ├── V1__Initialize_project_tables.sql │ │ └── V2__Insert_default_data.sql │ ├── static │ └── empty.txt │ └── templates │ └── empty.txt └── test ├── java └── kitchenpos │ ├── ApplicationTest.java │ ├── Fixtures.java │ ├── eatinorders │ └── application │ │ ├── FakeKitchenridersClient.java │ │ ├── InMemoryOrderRepository.java │ │ ├── InMemoryOrderTableRepository.java │ │ ├── OrderServiceTest.java │ │ └── OrderTableServiceTest.java │ ├── menus │ └── application │ │ ├── InMemoryMenuGroupRepository.java │ │ ├── InMemoryMenuRepository.java │ │ ├── MenuGroupServiceTest.java │ │ └── MenuServiceTest.java │ └── products │ └── application │ ├── FakePurgomalumClient.java │ ├── InMemoryProductRepository.java │ └── ProductServiceTest.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Kotlin ### 40 | .kotlin 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | 45 | ### Docker ### 46 | docker/db/mysql/data/ 47 | docker/db/mysql/init/* 48 | !docker/db/mysql/init/init.sql 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 키친포스 2 | 3 | ## 퀵 스타트 4 | 5 | ```sh 6 | cd docker 7 | docker compose -p kitchenpos up -d 8 | ``` 9 | 10 | ## 요구 사항 11 | 12 | ### 상품 13 | 14 | - 상품을 등록할 수 있다. 15 | - 상품의 가격이 올바르지 않으면 등록할 수 없다. 16 | - 상품의 가격은 0원 이상이어야 한다. 17 | - 상품의 이름이 올바르지 않으면 등록할 수 없다. 18 | - 상품의 이름에는 비속어가 포함될 수 없다. 19 | - 상품의 가격을 변경할 수 있다. 20 | - 상품의 가격이 올바르지 않으면 변경할 수 없다. 21 | - 상품의 가격은 0원 이상이어야 한다. 22 | - 상품의 가격이 변경될 때 메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 크면 메뉴가 숨겨진다. 23 | - 상품의 목록을 조회할 수 있다. 24 | 25 | ### 메뉴 그룹 26 | 27 | - 메뉴 그룹을 등록할 수 있다. 28 | - 메뉴 그룹의 이름이 올바르지 않으면 등록할 수 없다. 29 | - 메뉴 그룹의 이름은 비워 둘 수 없다. 30 | - 메뉴 그룹의 목록을 조회할 수 있다. 31 | 32 | ### 메뉴 33 | 34 | - 1 개 이상의 등록된 상품으로 메뉴를 등록할 수 있다. 35 | - 상품이 없으면 등록할 수 없다. 36 | - 메뉴에 속한 상품의 수량은 0 이상이어야 한다. 37 | - 메뉴의 가격이 올바르지 않으면 등록할 수 없다. 38 | - 메뉴의 가격은 0원 이상이어야 한다. 39 | - 메뉴에 속한 상품 금액의 합은 메뉴의 가격보다 크거나 같아야 한다. 40 | - 메뉴는 특정 메뉴 그룹에 속해야 한다. 41 | - 메뉴의 이름이 올바르지 않으면 등록할 수 없다. 42 | - 메뉴의 이름에는 비속어가 포함될 수 없다. 43 | - 메뉴의 가격을 변경할 수 있다. 44 | - 메뉴의 가격이 올바르지 않으면 변경할 수 없다. 45 | - 메뉴의 가격은 0원 이상이어야 한다. 46 | - 메뉴에 속한 상품 금액의 합은 메뉴의 가격보다 크거나 같아야 한다. 47 | - 메뉴를 노출할 수 있다. 48 | - 메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 높을 경우 메뉴를 노출할 수 없다. 49 | - 메뉴를 숨길 수 있다. 50 | - 메뉴의 목록을 조회할 수 있다. 51 | 52 | ### 주문 테이블 53 | 54 | - 주문 테이블을 등록할 수 있다. 55 | - 주문 테이블의 이름이 올바르지 않으면 등록할 수 없다. 56 | - 주문 테이블의 이름은 비워 둘 수 없다. 57 | - 빈 테이블을 해지할 수 있다. 58 | - 빈 테이블로 설정할 수 있다. 59 | - 완료되지 않은 주문이 있는 주문 테이블은 빈 테이블로 설정할 수 없다. 60 | - 방문한 손님 수를 변경할 수 있다. 61 | - 방문한 손님 수가 올바르지 않으면 변경할 수 없다. 62 | - 방문한 손님 수는 0 이상이어야 한다. 63 | - 빈 테이블은 방문한 손님 수를 변경할 수 없다. 64 | - 주문 테이블의 목록을 조회할 수 있다. 65 | 66 | ### 주문 67 | 68 | - 1개 이상의 등록된 메뉴로 배달 주문을 등록할 수 있다. 69 | - 1개 이상의 등록된 메뉴로 포장 주문을 등록할 수 있다. 70 | - 1개 이상의 등록된 메뉴로 매장 주문을 등록할 수 있다. 71 | - 주문 유형이 올바르지 않으면 등록할 수 없다. 72 | - 메뉴가 없으면 등록할 수 없다. 73 | - 매장 주문은 주문 항목의 수량이 0 미만일 수 있다. 74 | - 매장 주문을 제외한 주문의 경우 주문 항목의 수량은 0 이상이어야 한다. 75 | - 배달 주소가 올바르지 않으면 배달 주문을 등록할 수 없다. 76 | - 배달 주소는 비워 둘 수 없다. 77 | - 빈 테이블에는 매장 주문을 등록할 수 없다. 78 | - 숨겨진 메뉴는 주문할 수 없다. 79 | - 주문한 메뉴의 가격은 실제 메뉴 가격과 일치해야 한다. 80 | - 주문을 접수한다. 81 | - 접수 대기 중인 주문만 접수할 수 있다. 82 | - 배달 주문을 접수되면 배달 대행사를 호출한다. 83 | - 주문을 서빙한다. 84 | - 접수된 주문만 서빙할 수 있다. 85 | - 주문을 배달한다. 86 | - 배달 주문만 배달할 수 있다. 87 | - 서빙된 주문만 배달할 수 있다. 88 | - 주문을 배달 완료한다. 89 | - 배달 중인 주문만 배달 완료할 수 있다. 90 | - 주문을 완료한다. 91 | - 배달 주문의 경우 배달 완료된 주문만 완료할 수 있다. 92 | - 포장 및 매장 주문의 경우 서빙된 주문만 완료할 수 있다. 93 | - 주문 테이블의 모든 매장 주문이 완료되면 빈 테이블로 설정한다. 94 | - 완료되지 않은 매장 주문이 있는 주문 테이블은 빈 테이블로 설정하지 않는다. 95 | - 주문 목록을 조회할 수 있다. 96 | 97 | ## 용어 사전 98 | 99 | ### 상품 100 | 101 | | 한글명 | 영문명 | 설명 | 102 | | --- | --- | --- | 103 | | 상품 | product | 메뉴를 관리하는 기준이 되는 데이터 | 104 | | 이름 | displayed name | 음식을 상상하게 만드는 중요한 요소 | 105 | 106 | ### 메뉴 107 | 108 | | 한글명 | 영문명 | 설명 | 109 | | --- | --- | --- | 110 | | 금액 | amount | 가격 * 수량 | 111 | | 메뉴 | menu | 메뉴 그룹에 속하는 실제 주문 가능 단위 | 112 | | 메뉴 그룹 | menu group | 각각의 메뉴를 성격에 따라 분류하여 묶어둔 그룹 | 113 | | 메뉴 상품 | menu product | 메뉴에 속하는 수량이 있는 상품 | 114 | | 숨겨진 메뉴 | not displayed menu | 주문할 수 없는 숨겨진 메뉴 | 115 | | 이름 | displayed name | 음식을 상상하게 만드는 중요한 요소 | 116 | 117 | ### 매장 주문 118 | 119 | | 한글명 | 영문명 | 설명 | 120 | | --- | --- | --- | 121 | | 방문한 손님 수 | number of guests | 식기가 필요한 사람 수. 필수 사항은 아니며 주문은 0명으로 등록할 수 있다. | 122 | | 빈 테이블 | empty table | 주문을 등록할 수 없는 주문 테이블 | 123 | | 서빙 | served | 조리가 완료되어 음식이 나갈 수 있는 단계 | 124 | | 완료 | completed | 고객이 모든 식사를 마치고 결제를 완료한 단계 | 125 | | 접수 | accepted | 주문을 받고 음식을 조리하는 단계 | 126 | | 접수 대기 | waiting | 주문이 생성되어 매장으로 전달된 단계 | 127 | | 주문 | order | 매장에서 식사하는 고객 대상. 손님들이 매장에서 먹을 수 있도록 조리된 음식을 가져다준다. | 128 | | 주문 상태 | order status | 주문이 생성되면 매장에서 주문을 접수하고 고객이 음식을 받기까지의 단계를 표시한다. | 129 | | 주문 테이블 | order table | 매장에서 주문이 발생하는 영역 | 130 | | 주문 항목 | order line item | 주문에 속하는 수량이 있는 메뉴 | 131 | 132 | ### 배달 주문 133 | 134 | | 한글명 | 영문명 | 설명 | 135 | | --- | --- | --- | 136 | | 배달 | delivering | 배달원이 매장을 방문하여 배달 음식의 픽업을 완료하고 배달을 시작하는 단계 | 137 | | 배달 대행사 | delivery agency | 준비한 음식을 고객에게 직접 배달하는 서비스 | 138 | | 배달 완료 | delivered | 배달원이 주문한 음식을 고객에게 배달 완료한 단계 | 139 | | 서빙 | served | 조리가 완료되어 음식이 나갈 수 있는 단계 | 140 | | 완료 | completed | 배달 및 결제 완료 단계 | 141 | | 접수 | accepted | 주문을 받고 음식을 조리하는 단계 | 142 | | 접수 대기 | waiting | 주문이 생성되어 매장으로 전달된 단계 | 143 | | 주문 | order | 집이나 직장 등 고객이 선택한 주소로 음식을 배달한다. | 144 | | 주문 상태 | order status | 주문이 생성되면 매장에서 주문을 접수하고 고객이 음식을 받기까지의 단계를 표시한다. | 145 | | 주문 항목 | order line item | 주문에 속하는 수량이 있는 메뉴 | 146 | 147 | ### 포장 주문 148 | 149 | | 한글명 | 영문명 | 설명 | 150 | | --- | --- | --- | 151 | | 서빙 | served | 조리가 완료되어 음식이 나갈 수 있는 단계 | 152 | | 완료 | completed | 고객이 음식을 수령하고 결제를 완료한 단계 | 153 | | 접수 | accepted | 주문을 받고 음식을 조리하는 단계 | 154 | | 접수 대기 | waiting | 주문이 생성되어 매장으로 전달된 단계 | 155 | | 주문 | order | 포장하는 고객 대상. 고객이 매장에서 직접 음식을 수령한다. | 156 | | 주문 상태 | order status | 주문이 생성되면 매장에서 주문을 접수하고 고객이 음식을 받기까지의 단계를 표시한다. | 157 | | 주문 항목 | order line item | 주문에 속하는 수량이 있는 메뉴 | 158 | 159 | ## 모델링 160 | 161 | ### 상품 162 | 163 | - `Product`는 식별자와 `DisplayedName`, 가격을 가진다. 164 | - `DisplayedName`에는 `Profanity`가 포함될 수 없다. 165 | 166 | ### 메뉴 167 | 168 | - `MenuGroup`은 식별자와 이름을 가진다. 169 | - `Menu`는 식별자와 `Displayed Name`, 가격, `MenuProducts`를 가진다. 170 | - `Menu`는 특정 `MenuGroup`에 속한다. 171 | - `Menu`의 가격은 `MenuProducts`의 금액의 합보다 적거나 같아야 한다. 172 | - `Menu`의 가격이 `MenuProducts`의 금액의 합보다 크면 `NotDisplayedMenu`가 된다. 173 | - `MenuProduct`는 가격과 수량을 가진다. 174 | 175 | ### 매장 주문 176 | 177 | - `OrderTable`은 식별자와 이름, `NumberOfGuests`를 가진다. 178 | - `OrderTable`의 추가 `Order`는 `OrderTable`에 계속 쌓이며 모든 `Order`가 완료되면 `EmptyTable`이 된다. 179 | - `EmptyTable`인 경우 `NumberOfGuests`는 0이며 변경할 수 없다. 180 | - `Order`는 식별자와 `OrderStatus`, 주문 시간, `OrderLineItems`를 가진다. 181 | - 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Order`가 생성된다. 182 | - `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다. 183 | - `OrderLineItem`는 가격과 수량을 가진다. 184 | - `OrderLineItem`의 수량은 기존 `Order`를 취소하거나 변경해도 수정되지 않기 때문에 0보다 적을 수 있다. 185 | 186 | ### 배달 주문 187 | 188 | - `Order`는 식별자와 `OrderStatus`, 주문 시간, 배달 주소, `OrderLineItems`를 가진다. 189 | - 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Order`가 생성된다. 190 | - `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 배달 ➜ 배달 완료 ➜ 계산 완료 순서로 진행된다. 191 | - `Order`가 접수되면 `DeliveryAgency`가 호출된다. 192 | - `OrderLineItem`는 가격과 수량을 가진다. 193 | - `OrderLineItem`의 수량은 1보다 커야 한다. 194 | 195 | ### 포장 주문 196 | 197 | - `Order`는 식별자와 `OrderStatus`, 주문 시간, `OrderLineItems`를 가진다. 198 | - 메뉴가 노출되고 있으며 판매되는 메뉴 가격과 일치하면 `Order`가 생성된다. 199 | - `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다. 200 | - `OrderLineItem`는 가격과 수량을 가진다. 201 | - `OrderLineItem`의 수량은 1보다 커야 한다. 202 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("org.springframework.boot") version "3.2.5" 5 | id("io.spring.dependency-management") version "1.1.4" 6 | kotlin("jvm") version "1.9.23" 7 | kotlin("plugin.spring") version "1.9.23" 8 | kotlin("plugin.jpa") version "1.9.23" 9 | id("org.flywaydb.flyway") version "7.12.0" 10 | } 11 | 12 | group = "camp.nextstep.edu" 13 | version = "0.0.1-SNAPSHOT" 14 | 15 | java { 16 | sourceCompatibility = JavaVersion.VERSION_21 17 | } 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 25 | implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 26 | implementation("org.springframework.boot:spring-boot-starter-validation") 27 | implementation("org.springframework.boot:spring-boot-starter-web") 28 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin") 29 | implementation("org.flywaydb:flyway-core") 30 | implementation("org.flywaydb:flyway-mysql") 31 | implementation("org.jetbrains.kotlin:kotlin-reflect") 32 | runtimeOnly("com.h2database:h2") 33 | runtimeOnly("com.mysql:mysql-connector-j") 34 | testImplementation("org.springframework.boot:spring-boot-starter-test") 35 | } 36 | 37 | tasks.withType { 38 | kotlinOptions { 39 | freeCompilerArgs += "-Xjsr305=strict" 40 | jvmTarget = "21" 41 | } 42 | } 43 | 44 | tasks.withType { 45 | useJUnitPlatform() 46 | } 47 | 48 | flyway { 49 | url = "jdbc:mysql://localhost:33306/kitchenpos" 50 | user = "user" 51 | password = "password" 52 | } 53 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: kitchenpos 2 | services: 3 | db: 4 | image: mysql:8.0.30 5 | platform: linux/x86_64 6 | restart: always 7 | ports: 8 | - "33306:3306" 9 | environment: 10 | MYSQL_ROOT_PASSWORD: root 11 | MYSQL_DATABASE: kitchenpos 12 | MYSQL_USER: user 13 | MYSQL_PASSWORD: password 14 | TZ: Asia/Seoul 15 | volumes: 16 | - ./db/mysql/data:/var/lib/mysql 17 | - ./db/mysql/config:/etc/mysql/conf.d 18 | - ./db/mysql/init:/docker-entrypoint-initdb.d 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/ddd-tactical-design/f712c32c9f727a24e41015467b47c8700caf930b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /http/menu-groups.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST {{host}}/api/menu-groups 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "추천메뉴" 7 | } 8 | 9 | ### 10 | GET {{host}}/api/menu-groups 11 | -------------------------------------------------------------------------------- /http/menus.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST {{host}}/api/menus 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "후라이드+후라이드", 7 | "price": 19000, 8 | "menuGroupId": "f1860abc-2ea1-411b-bd4a-baa44f0d5580", 9 | "displayed": true, 10 | "menuProducts": [ 11 | { 12 | "productId": "3b528244-34f7-406b-bb7e-690912f66b10", 13 | "quantity": 2 14 | } 15 | ] 16 | } 17 | 18 | ### 19 | PUT {{host}}/api/menus/f59b1e1c-b145-440a-aa6f-6095a0e2d63b/price 20 | Content-Type: application/json 21 | 22 | { 23 | "price": 15000 24 | } 25 | 26 | ### 27 | PUT {{host}}/api/menus/f59b1e1c-b145-440a-aa6f-6095a0e2d63b/display 28 | 29 | ### 30 | PUT {{host}}/api/menus/f59b1e1c-b145-440a-aa6f-6095a0e2d63b/hide 31 | 32 | ### 33 | GET {{host}}/api/menus 34 | -------------------------------------------------------------------------------- /http/order-tables.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST {{host}}/api/order-tables 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "9번" 7 | } 8 | 9 | ### 10 | PUT {{host}}/api/order-tables/8d710043-29b6-420e-8452-233f5a035520/sit 11 | 12 | ### 13 | PUT {{host}}/api/order-tables/8d710043-29b6-420e-8452-233f5a035520/clear 14 | 15 | ### 16 | PUT {{host}}/api/order-tables/8d710043-29b6-420e-8452-233f5a035520/number-of-guests 17 | Content-Type: application/json 18 | 19 | { 20 | "numberOfGuests": 4 21 | } 22 | 23 | ### 24 | GET {{host}}/api/order-tables 25 | -------------------------------------------------------------------------------- /http/orders.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST {{host}}/api/orders 3 | Content-Type: application/json 4 | 5 | { 6 | "type": "EAT_IN", 7 | "orderTableId": "8d710043-29b6-420e-8452-233f5a035520", 8 | "orderLineItems": [ 9 | { 10 | "menuId": "f59b1e1c-b145-440a-aa6f-6095a0e2d63b", 11 | "price": 16000, 12 | "quantity": 3 13 | } 14 | ] 15 | } 16 | 17 | ### 18 | PUT {{host}}/api/orders/69d78f38-3bff-457c-bb72-26319c985fd8/accept 19 | 20 | ### 21 | PUT {{host}}/api/orders/69d78f38-3bff-457c-bb72-26319c985fd8/serve 22 | 23 | ### 24 | PUT {{host}}/api/orders/69d78f38-3bff-457c-bb72-26319c985fd8/start-delivery 25 | 26 | ### 27 | PUT {{host}}/api/orders/69d78f38-3bff-457c-bb72-26319c985fd8/complete-delivery 28 | 29 | ### 30 | PUT {{host}}/api/orders/69d78f38-3bff-457c-bb72-26319c985fd8/complete 31 | 32 | ### 33 | GET {{host}}/api/orders 34 | -------------------------------------------------------------------------------- /http/products.http: -------------------------------------------------------------------------------- 1 | ### 2 | POST {{host}}/api/products 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "강정치킨", 7 | "price": 17000 8 | } 9 | 10 | ### 11 | PUT {{host}}/api/products/3b528244-34f7-406b-bb7e-690912f66b10/price 12 | Content-Type: application/json 13 | 14 | { 15 | "price": 18000 16 | } 17 | 18 | ### 19 | GET {{host}}/api/products 20 | -------------------------------------------------------------------------------- /rest-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "host": "localhost:8080" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ddd-kitchenpos" 2 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/Application.java: -------------------------------------------------------------------------------- 1 | package kitchenpos; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Application.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/deliveryorders/infra/DefaultKitchenridersClient.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.deliveryorders.infra; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.UUID; 7 | 8 | @Component 9 | public class DefaultKitchenridersClient implements KitchenridersClient { 10 | @Override 11 | public void requestDelivery(final UUID orderId, final BigDecimal amount, final String deliveryAddress) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/deliveryorders/infra/KitchenridersClient.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.deliveryorders.infra; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | 6 | public interface KitchenridersClient { 7 | void requestDelivery(UUID orderId, BigDecimal amount, String deliveryAddress); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/application/OrderService.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.deliveryorders.infra.KitchenridersClient; 4 | import kitchenpos.eatinorders.domain.Order; 5 | import kitchenpos.eatinorders.domain.OrderLineItem; 6 | import kitchenpos.eatinorders.domain.OrderRepository; 7 | import kitchenpos.eatinorders.domain.OrderStatus; 8 | import kitchenpos.eatinorders.domain.OrderTable; 9 | import kitchenpos.eatinorders.domain.OrderTableRepository; 10 | import kitchenpos.eatinorders.domain.OrderType; 11 | import kitchenpos.menus.domain.Menu; 12 | import kitchenpos.menus.domain.MenuRepository; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import java.math.BigDecimal; 17 | import java.time.LocalDateTime; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.NoSuchElementException; 21 | import java.util.Objects; 22 | import java.util.UUID; 23 | 24 | @Service 25 | public class OrderService { 26 | private final OrderRepository orderRepository; 27 | private final MenuRepository menuRepository; 28 | private final OrderTableRepository orderTableRepository; 29 | private final KitchenridersClient kitchenridersClient; 30 | 31 | public OrderService( 32 | final OrderRepository orderRepository, 33 | final MenuRepository menuRepository, 34 | final OrderTableRepository orderTableRepository, 35 | final KitchenridersClient kitchenridersClient 36 | ) { 37 | this.orderRepository = orderRepository; 38 | this.menuRepository = menuRepository; 39 | this.orderTableRepository = orderTableRepository; 40 | this.kitchenridersClient = kitchenridersClient; 41 | } 42 | 43 | @Transactional 44 | public Order create(final Order request) { 45 | final OrderType type = request.getType(); 46 | if (Objects.isNull(type)) { 47 | throw new IllegalArgumentException(); 48 | } 49 | final List orderLineItemRequests = request.getOrderLineItems(); 50 | if (Objects.isNull(orderLineItemRequests) || orderLineItemRequests.isEmpty()) { 51 | throw new IllegalArgumentException(); 52 | } 53 | final List menus = menuRepository.findAllByIdIn( 54 | orderLineItemRequests.stream() 55 | .map(OrderLineItem::getMenuId) 56 | .toList() 57 | ); 58 | if (menus.size() != orderLineItemRequests.size()) { 59 | throw new IllegalArgumentException(); 60 | } 61 | final List orderLineItems = new ArrayList<>(); 62 | for (final OrderLineItem orderLineItemRequest : orderLineItemRequests) { 63 | final long quantity = orderLineItemRequest.getQuantity(); 64 | if (type != OrderType.EAT_IN) { 65 | if (quantity < 0) { 66 | throw new IllegalArgumentException(); 67 | } 68 | } 69 | final Menu menu = menuRepository.findById(orderLineItemRequest.getMenuId()) 70 | .orElseThrow(NoSuchElementException::new); 71 | if (!menu.isDisplayed()) { 72 | throw new IllegalStateException(); 73 | } 74 | if (menu.getPrice().compareTo(orderLineItemRequest.getPrice()) != 0) { 75 | throw new IllegalArgumentException(); 76 | } 77 | final OrderLineItem orderLineItem = new OrderLineItem(); 78 | orderLineItem.setMenu(menu); 79 | orderLineItem.setQuantity(quantity); 80 | orderLineItems.add(orderLineItem); 81 | } 82 | Order order = new Order(); 83 | order.setId(UUID.randomUUID()); 84 | order.setType(type); 85 | order.setStatus(OrderStatus.WAITING); 86 | order.setOrderDateTime(LocalDateTime.now()); 87 | order.setOrderLineItems(orderLineItems); 88 | if (type == OrderType.DELIVERY) { 89 | final String deliveryAddress = request.getDeliveryAddress(); 90 | if (Objects.isNull(deliveryAddress) || deliveryAddress.isEmpty()) { 91 | throw new IllegalArgumentException(); 92 | } 93 | order.setDeliveryAddress(deliveryAddress); 94 | } 95 | if (type == OrderType.EAT_IN) { 96 | final OrderTable orderTable = orderTableRepository.findById(request.getOrderTableId()) 97 | .orElseThrow(NoSuchElementException::new); 98 | if (!orderTable.isOccupied()) { 99 | throw new IllegalStateException(); 100 | } 101 | order.setOrderTable(orderTable); 102 | } 103 | return orderRepository.save(order); 104 | } 105 | 106 | @Transactional 107 | public Order accept(final UUID orderId) { 108 | final Order order = orderRepository.findById(orderId) 109 | .orElseThrow(NoSuchElementException::new); 110 | if (order.getStatus() != OrderStatus.WAITING) { 111 | throw new IllegalStateException(); 112 | } 113 | if (order.getType() == OrderType.DELIVERY) { 114 | BigDecimal sum = BigDecimal.ZERO; 115 | for (final OrderLineItem orderLineItem : order.getOrderLineItems()) { 116 | sum = orderLineItem.getMenu() 117 | .getPrice() 118 | .multiply(BigDecimal.valueOf(orderLineItem.getQuantity())); 119 | } 120 | kitchenridersClient.requestDelivery(orderId, sum, order.getDeliveryAddress()); 121 | } 122 | order.setStatus(OrderStatus.ACCEPTED); 123 | return order; 124 | } 125 | 126 | @Transactional 127 | public Order serve(final UUID orderId) { 128 | final Order order = orderRepository.findById(orderId) 129 | .orElseThrow(NoSuchElementException::new); 130 | if (order.getStatus() != OrderStatus.ACCEPTED) { 131 | throw new IllegalStateException(); 132 | } 133 | order.setStatus(OrderStatus.SERVED); 134 | return order; 135 | } 136 | 137 | @Transactional 138 | public Order startDelivery(final UUID orderId) { 139 | final Order order = orderRepository.findById(orderId) 140 | .orElseThrow(NoSuchElementException::new); 141 | if (order.getType() != OrderType.DELIVERY) { 142 | throw new IllegalStateException(); 143 | } 144 | if (order.getStatus() != OrderStatus.SERVED) { 145 | throw new IllegalStateException(); 146 | } 147 | order.setStatus(OrderStatus.DELIVERING); 148 | return order; 149 | } 150 | 151 | @Transactional 152 | public Order completeDelivery(final UUID orderId) { 153 | final Order order = orderRepository.findById(orderId) 154 | .orElseThrow(NoSuchElementException::new); 155 | if (order.getStatus() != OrderStatus.DELIVERING) { 156 | throw new IllegalStateException(); 157 | } 158 | order.setStatus(OrderStatus.DELIVERED); 159 | return order; 160 | } 161 | 162 | @Transactional 163 | public Order complete(final UUID orderId) { 164 | final Order order = orderRepository.findById(orderId) 165 | .orElseThrow(NoSuchElementException::new); 166 | final OrderType type = order.getType(); 167 | final OrderStatus status = order.getStatus(); 168 | if (type == OrderType.DELIVERY) { 169 | if (status != OrderStatus.DELIVERED) { 170 | throw new IllegalStateException(); 171 | } 172 | } 173 | if (type == OrderType.TAKEOUT || type == OrderType.EAT_IN) { 174 | if (status != OrderStatus.SERVED) { 175 | throw new IllegalStateException(); 176 | } 177 | } 178 | order.setStatus(OrderStatus.COMPLETED); 179 | if (type == OrderType.EAT_IN) { 180 | final OrderTable orderTable = order.getOrderTable(); 181 | if (!orderRepository.existsByOrderTableAndStatusNot(orderTable, OrderStatus.COMPLETED)) { 182 | orderTable.setNumberOfGuests(0); 183 | orderTable.setOccupied(false); 184 | } 185 | } 186 | return order; 187 | } 188 | 189 | @Transactional(readOnly = true) 190 | public List findAll() { 191 | return orderRepository.findAll(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/application/OrderTableService.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.eatinorders.domain.OrderRepository; 4 | import kitchenpos.eatinorders.domain.OrderStatus; 5 | import kitchenpos.eatinorders.domain.OrderTable; 6 | import kitchenpos.eatinorders.domain.OrderTableRepository; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import java.util.List; 11 | import java.util.NoSuchElementException; 12 | import java.util.Objects; 13 | import java.util.UUID; 14 | 15 | @Service 16 | public class OrderTableService { 17 | private final OrderTableRepository orderTableRepository; 18 | private final OrderRepository orderRepository; 19 | 20 | public OrderTableService(final OrderTableRepository orderTableRepository, final OrderRepository orderRepository) { 21 | this.orderTableRepository = orderTableRepository; 22 | this.orderRepository = orderRepository; 23 | } 24 | 25 | @Transactional 26 | public OrderTable create(final OrderTable request) { 27 | final String name = request.getName(); 28 | if (Objects.isNull(name) || name.isEmpty()) { 29 | throw new IllegalArgumentException(); 30 | } 31 | final OrderTable orderTable = new OrderTable(); 32 | orderTable.setId(UUID.randomUUID()); 33 | orderTable.setName(name); 34 | orderTable.setNumberOfGuests(0); 35 | orderTable.setOccupied(false); 36 | return orderTableRepository.save(orderTable); 37 | } 38 | 39 | @Transactional 40 | public OrderTable sit(final UUID orderTableId) { 41 | final OrderTable orderTable = orderTableRepository.findById(orderTableId) 42 | .orElseThrow(NoSuchElementException::new); 43 | orderTable.setOccupied(true); 44 | return orderTable; 45 | } 46 | 47 | @Transactional 48 | public OrderTable clear(final UUID orderTableId) { 49 | final OrderTable orderTable = orderTableRepository.findById(orderTableId) 50 | .orElseThrow(NoSuchElementException::new); 51 | if (orderRepository.existsByOrderTableAndStatusNot(orderTable, OrderStatus.COMPLETED)) { 52 | throw new IllegalStateException(); 53 | } 54 | orderTable.setNumberOfGuests(0); 55 | orderTable.setOccupied(false); 56 | return orderTable; 57 | } 58 | 59 | @Transactional 60 | public OrderTable changeNumberOfGuests(final UUID orderTableId, final OrderTable request) { 61 | final int numberOfGuests = request.getNumberOfGuests(); 62 | if (numberOfGuests < 0) { 63 | throw new IllegalArgumentException(); 64 | } 65 | final OrderTable orderTable = orderTableRepository.findById(orderTableId) 66 | .orElseThrow(NoSuchElementException::new); 67 | if (!orderTable.isOccupied()) { 68 | throw new IllegalStateException(); 69 | } 70 | orderTable.setNumberOfGuests(numberOfGuests); 71 | return orderTable; 72 | } 73 | 74 | @Transactional(readOnly = true) 75 | public List findAll() { 76 | return orderTableRepository.findAll(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/JpaOrderRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.UUID; 6 | 7 | public interface JpaOrderRepository extends OrderRepository, JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/JpaOrderTableRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.UUID; 6 | 7 | public interface JpaOrderTableRepository extends OrderTableRepository, JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/Order.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.EnumType; 7 | import jakarta.persistence.Enumerated; 8 | import jakarta.persistence.ForeignKey; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.JoinColumn; 11 | import jakarta.persistence.ManyToOne; 12 | import jakarta.persistence.OneToMany; 13 | import jakarta.persistence.Table; 14 | import jakarta.persistence.Transient; 15 | 16 | import java.time.LocalDateTime; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | @Table(name = "orders") 21 | @Entity 22 | public class Order { 23 | @Column(name = "id", columnDefinition = "binary(16)") 24 | @Id 25 | private UUID id; 26 | 27 | @Column(name = "type", nullable = false, columnDefinition = "varchar(255)") 28 | @Enumerated(EnumType.STRING) 29 | private OrderType type; 30 | 31 | @Column(name = "status", nullable = false, columnDefinition = "varchar(255)") 32 | @Enumerated(EnumType.STRING) 33 | private OrderStatus status; 34 | 35 | @Column(name = "order_date_time", nullable = false) 36 | private LocalDateTime orderDateTime; 37 | 38 | @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) 39 | @JoinColumn( 40 | name = "order_id", 41 | nullable = false, 42 | columnDefinition = "binary(16)", 43 | foreignKey = @ForeignKey(name = "fk_order_line_item_to_orders") 44 | ) 45 | private List orderLineItems; 46 | 47 | @Column(name = "delivery_address") 48 | private String deliveryAddress; 49 | 50 | @ManyToOne 51 | @JoinColumn( 52 | name = "order_table_id", 53 | columnDefinition = "binary(16)", 54 | foreignKey = @ForeignKey(name = "fk_orders_to_order_table") 55 | ) 56 | private OrderTable orderTable; 57 | 58 | @Transient 59 | private UUID orderTableId; 60 | 61 | public Order() { 62 | } 63 | 64 | public UUID getId() { 65 | return id; 66 | } 67 | 68 | public void setId(final UUID id) { 69 | this.id = id; 70 | } 71 | 72 | public OrderType getType() { 73 | return type; 74 | } 75 | 76 | public void setType(final OrderType type) { 77 | this.type = type; 78 | } 79 | 80 | public OrderStatus getStatus() { 81 | return status; 82 | } 83 | 84 | public void setStatus(final OrderStatus status) { 85 | this.status = status; 86 | } 87 | 88 | public LocalDateTime getOrderDateTime() { 89 | return orderDateTime; 90 | } 91 | 92 | public void setOrderDateTime(final LocalDateTime orderDateTime) { 93 | this.orderDateTime = orderDateTime; 94 | } 95 | 96 | public List getOrderLineItems() { 97 | return orderLineItems; 98 | } 99 | 100 | public void setOrderLineItems(final List orderLineItems) { 101 | this.orderLineItems = orderLineItems; 102 | } 103 | 104 | public String getDeliveryAddress() { 105 | return deliveryAddress; 106 | } 107 | 108 | public void setDeliveryAddress(final String deliveryAddress) { 109 | this.deliveryAddress = deliveryAddress; 110 | } 111 | 112 | public OrderTable getOrderTable() { 113 | return orderTable; 114 | } 115 | 116 | public void setOrderTable(final OrderTable orderTable) { 117 | this.orderTable = orderTable; 118 | } 119 | 120 | public UUID getOrderTableId() { 121 | return orderTableId; 122 | } 123 | 124 | public void setOrderTableId(final UUID orderTableId) { 125 | this.orderTableId = orderTableId; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/OrderLineItem.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.ForeignKey; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.Table; 12 | import jakarta.persistence.Transient; 13 | import kitchenpos.menus.domain.Menu; 14 | 15 | import java.math.BigDecimal; 16 | import java.util.UUID; 17 | 18 | @Table(name = "order_line_item") 19 | @Entity 20 | public class OrderLineItem { 21 | @Column(name = "seq") 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Id 24 | private Long seq; 25 | 26 | @ManyToOne(optional = false) 27 | @JoinColumn( 28 | name = "menu_id", 29 | columnDefinition = "binary(16)", 30 | foreignKey = @ForeignKey(name = "fk_order_line_item_to_menu") 31 | ) 32 | private Menu menu; 33 | 34 | @Column(name = "quantity", nullable = false) 35 | private long quantity; 36 | 37 | @Transient 38 | private UUID menuId; 39 | 40 | @Transient 41 | private BigDecimal price; 42 | 43 | public OrderLineItem() { 44 | } 45 | 46 | public Long getSeq() { 47 | return seq; 48 | } 49 | 50 | public void setSeq(final Long seq) { 51 | this.seq = seq; 52 | } 53 | 54 | public Menu getMenu() { 55 | return menu; 56 | } 57 | 58 | public void setMenu(final Menu menu) { 59 | this.menu = menu; 60 | } 61 | 62 | public long getQuantity() { 63 | return quantity; 64 | } 65 | 66 | public void setQuantity(final long quantity) { 67 | this.quantity = quantity; 68 | } 69 | 70 | public UUID getMenuId() { 71 | return menuId; 72 | } 73 | 74 | public void setMenuId(final UUID menuId) { 75 | this.menuId = menuId; 76 | } 77 | 78 | public BigDecimal getPrice() { 79 | return price; 80 | } 81 | 82 | public void setPrice(final BigDecimal price) { 83 | this.price = price; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | public interface OrderRepository { 8 | Order save(Order order); 9 | 10 | Optional findById(UUID id); 11 | 12 | List findAll(); 13 | 14 | boolean existsByOrderTableAndStatusNot(OrderTable orderTable, OrderStatus status); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | public enum OrderStatus { 4 | WAITING, ACCEPTED, SERVED, DELIVERING, DELIVERED, COMPLETED 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/OrderTable.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.Table; 7 | 8 | import java.util.UUID; 9 | 10 | @Table(name = "order_table") 11 | @Entity 12 | public class OrderTable { 13 | @Column(name = "id", columnDefinition = "binary(16)") 14 | @Id 15 | private UUID id; 16 | 17 | @Column(name = "name", nullable = false) 18 | private String name; 19 | 20 | @Column(name = "number_of_guests", nullable = false) 21 | private int numberOfGuests; 22 | 23 | @Column(name = "occupied", nullable = false) 24 | private boolean occupied; 25 | 26 | public OrderTable() { 27 | } 28 | 29 | public UUID getId() { 30 | return id; 31 | } 32 | 33 | public void setId(final UUID id) { 34 | this.id = id; 35 | } 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public void setName(final String name) { 42 | this.name = name; 43 | } 44 | 45 | public int getNumberOfGuests() { 46 | return numberOfGuests; 47 | } 48 | 49 | public void setNumberOfGuests(final int numberOfGuests) { 50 | this.numberOfGuests = numberOfGuests; 51 | } 52 | 53 | public boolean isOccupied() { 54 | return occupied; 55 | } 56 | 57 | public void setOccupied(final boolean occupied) { 58 | this.occupied = occupied; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/OrderTableRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | public interface OrderTableRepository { 8 | OrderTable save(OrderTable orderTable); 9 | 10 | Optional findById(UUID id); 11 | 12 | List findAll(); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/domain/OrderType.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.domain; 2 | 3 | public enum OrderType { 4 | DELIVERY, TAKEOUT, EAT_IN 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/ui/OrderRestController.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.ui; 2 | 3 | import kitchenpos.eatinorders.application.OrderService; 4 | import kitchenpos.eatinorders.domain.Order; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.net.URI; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | @RequestMapping("/api/orders") 19 | @RestController 20 | public class OrderRestController { 21 | private final OrderService orderService; 22 | 23 | public OrderRestController(final OrderService orderService) { 24 | this.orderService = orderService; 25 | } 26 | 27 | @PostMapping 28 | public ResponseEntity create(@RequestBody final Order request) { 29 | final Order response = orderService.create(request); 30 | return ResponseEntity.created(URI.create("/api/orders/" + response.getId())) 31 | .body(response); 32 | } 33 | 34 | @PutMapping("/{orderId}/accept") 35 | public ResponseEntity accept(@PathVariable final UUID orderId) { 36 | return ResponseEntity.ok(orderService.accept(orderId)); 37 | } 38 | 39 | @PutMapping("/{orderId}/serve") 40 | public ResponseEntity serve(@PathVariable final UUID orderId) { 41 | return ResponseEntity.ok(orderService.serve(orderId)); 42 | } 43 | 44 | @PutMapping("/{orderId}/start-delivery") 45 | public ResponseEntity startDelivery(@PathVariable final UUID orderId) { 46 | return ResponseEntity.ok(orderService.startDelivery(orderId)); 47 | } 48 | 49 | @PutMapping("/{orderId}/complete-delivery") 50 | public ResponseEntity completeDelivery(@PathVariable final UUID orderId) { 51 | return ResponseEntity.ok(orderService.completeDelivery(orderId)); 52 | } 53 | 54 | @PutMapping("/{orderId}/complete") 55 | public ResponseEntity complete(@PathVariable final UUID orderId) { 56 | return ResponseEntity.ok(orderService.complete(orderId)); 57 | } 58 | 59 | @GetMapping 60 | public ResponseEntity> findAll() { 61 | return ResponseEntity.ok(orderService.findAll()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/eatinorders/ui/OrderTableRestController.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.ui; 2 | 3 | import kitchenpos.eatinorders.application.OrderTableService; 4 | import kitchenpos.eatinorders.domain.OrderTable; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.net.URI; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | @RequestMapping("/api/order-tables") 19 | @RestController 20 | public class OrderTableRestController { 21 | private final OrderTableService orderTableService; 22 | 23 | public OrderTableRestController(final OrderTableService orderTableService) { 24 | this.orderTableService = orderTableService; 25 | } 26 | 27 | @PostMapping 28 | public ResponseEntity create(@RequestBody final OrderTable request) { 29 | final OrderTable response = orderTableService.create(request); 30 | return ResponseEntity.created(URI.create("/api/order-tables/" + response.getId())) 31 | .body(response); 32 | } 33 | 34 | @PutMapping("/{orderTableId}/sit") 35 | public ResponseEntity sit(@PathVariable final UUID orderTableId) { 36 | return ResponseEntity.ok(orderTableService.sit(orderTableId)); 37 | } 38 | 39 | @PutMapping("/{orderTableId}/clear") 40 | public ResponseEntity clear(@PathVariable final UUID orderTableId) { 41 | return ResponseEntity.ok(orderTableService.clear(orderTableId)); 42 | } 43 | 44 | @PutMapping("/{orderTableId}/number-of-guests") 45 | public ResponseEntity changeNumberOfGuests( 46 | @PathVariable final UUID orderTableId, 47 | @RequestBody final OrderTable request 48 | ) { 49 | return ResponseEntity.ok(orderTableService.changeNumberOfGuests(orderTableId, request)); 50 | } 51 | 52 | @GetMapping 53 | public ResponseEntity> findAll() { 54 | return ResponseEntity.ok(orderTableService.findAll()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/application/MenuGroupService.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.application; 2 | 3 | import kitchenpos.menus.domain.MenuGroup; 4 | import kitchenpos.menus.domain.MenuGroupRepository; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import java.util.List; 9 | import java.util.Objects; 10 | import java.util.UUID; 11 | 12 | @Service 13 | public class MenuGroupService { 14 | private final MenuGroupRepository menuGroupRepository; 15 | 16 | public MenuGroupService(final MenuGroupRepository menuGroupRepository) { 17 | this.menuGroupRepository = menuGroupRepository; 18 | } 19 | 20 | @Transactional 21 | public MenuGroup create(final MenuGroup request) { 22 | final String name = request.getName(); 23 | if (Objects.isNull(name) || name.isEmpty()) { 24 | throw new IllegalArgumentException(); 25 | } 26 | final MenuGroup menuGroup = new MenuGroup(); 27 | menuGroup.setId(UUID.randomUUID()); 28 | menuGroup.setName(name); 29 | return menuGroupRepository.save(menuGroup); 30 | } 31 | 32 | @Transactional(readOnly = true) 33 | public List findAll() { 34 | return menuGroupRepository.findAll(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/application/MenuService.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.application; 2 | 3 | import kitchenpos.menus.domain.Menu; 4 | import kitchenpos.menus.domain.MenuGroup; 5 | import kitchenpos.menus.domain.MenuGroupRepository; 6 | import kitchenpos.menus.domain.MenuProduct; 7 | import kitchenpos.menus.domain.MenuRepository; 8 | import kitchenpos.products.domain.Product; 9 | import kitchenpos.products.domain.ProductRepository; 10 | import kitchenpos.products.infra.PurgomalumClient; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import java.math.BigDecimal; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.NoSuchElementException; 18 | import java.util.Objects; 19 | import java.util.UUID; 20 | 21 | @Service 22 | public class MenuService { 23 | private final MenuRepository menuRepository; 24 | private final MenuGroupRepository menuGroupRepository; 25 | private final ProductRepository productRepository; 26 | private final PurgomalumClient purgomalumClient; 27 | 28 | public MenuService( 29 | final MenuRepository menuRepository, 30 | final MenuGroupRepository menuGroupRepository, 31 | final ProductRepository productRepository, 32 | final PurgomalumClient purgomalumClient 33 | ) { 34 | this.menuRepository = menuRepository; 35 | this.menuGroupRepository = menuGroupRepository; 36 | this.productRepository = productRepository; 37 | this.purgomalumClient = purgomalumClient; 38 | } 39 | 40 | @Transactional 41 | public Menu create(final Menu request) { 42 | final BigDecimal price = request.getPrice(); 43 | if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { 44 | throw new IllegalArgumentException(); 45 | } 46 | final MenuGroup menuGroup = menuGroupRepository.findById(request.getMenuGroupId()) 47 | .orElseThrow(NoSuchElementException::new); 48 | final List menuProductRequests = request.getMenuProducts(); 49 | if (Objects.isNull(menuProductRequests) || menuProductRequests.isEmpty()) { 50 | throw new IllegalArgumentException(); 51 | } 52 | final List products = productRepository.findAllByIdIn( 53 | menuProductRequests.stream() 54 | .map(MenuProduct::getProductId) 55 | .toList() 56 | ); 57 | if (products.size() != menuProductRequests.size()) { 58 | throw new IllegalArgumentException(); 59 | } 60 | final List menuProducts = new ArrayList<>(); 61 | BigDecimal sum = BigDecimal.ZERO; 62 | for (final MenuProduct menuProductRequest : menuProductRequests) { 63 | final long quantity = menuProductRequest.getQuantity(); 64 | if (quantity < 0) { 65 | throw new IllegalArgumentException(); 66 | } 67 | final Product product = productRepository.findById(menuProductRequest.getProductId()) 68 | .orElseThrow(NoSuchElementException::new); 69 | sum = sum.add( 70 | product.getPrice() 71 | .multiply(BigDecimal.valueOf(quantity)) 72 | ); 73 | final MenuProduct menuProduct = new MenuProduct(); 74 | menuProduct.setProduct(product); 75 | menuProduct.setQuantity(quantity); 76 | menuProducts.add(menuProduct); 77 | } 78 | if (price.compareTo(sum) > 0) { 79 | throw new IllegalArgumentException(); 80 | } 81 | final String name = request.getName(); 82 | if (Objects.isNull(name) || purgomalumClient.containsProfanity(name)) { 83 | throw new IllegalArgumentException(); 84 | } 85 | final Menu menu = new Menu(); 86 | menu.setId(UUID.randomUUID()); 87 | menu.setName(name); 88 | menu.setPrice(price); 89 | menu.setMenuGroup(menuGroup); 90 | menu.setDisplayed(request.isDisplayed()); 91 | menu.setMenuProducts(menuProducts); 92 | return menuRepository.save(menu); 93 | } 94 | 95 | @Transactional 96 | public Menu changePrice(final UUID menuId, final Menu request) { 97 | final BigDecimal price = request.getPrice(); 98 | if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { 99 | throw new IllegalArgumentException(); 100 | } 101 | final Menu menu = menuRepository.findById(menuId) 102 | .orElseThrow(NoSuchElementException::new); 103 | BigDecimal sum = BigDecimal.ZERO; 104 | for (final MenuProduct menuProduct : menu.getMenuProducts()) { 105 | sum = sum.add( 106 | menuProduct.getProduct() 107 | .getPrice() 108 | .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) 109 | ); 110 | } 111 | if (price.compareTo(sum) > 0) { 112 | throw new IllegalArgumentException(); 113 | } 114 | menu.setPrice(price); 115 | return menu; 116 | } 117 | 118 | @Transactional 119 | public Menu display(final UUID menuId) { 120 | final Menu menu = menuRepository.findById(menuId) 121 | .orElseThrow(NoSuchElementException::new); 122 | BigDecimal sum = BigDecimal.ZERO; 123 | for (final MenuProduct menuProduct : menu.getMenuProducts()) { 124 | sum = sum.add( 125 | menuProduct.getProduct() 126 | .getPrice() 127 | .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) 128 | ); 129 | } 130 | if (menu.getPrice().compareTo(sum) > 0) { 131 | throw new IllegalStateException(); 132 | } 133 | menu.setDisplayed(true); 134 | return menu; 135 | } 136 | 137 | @Transactional 138 | public Menu hide(final UUID menuId) { 139 | final Menu menu = menuRepository.findById(menuId) 140 | .orElseThrow(NoSuchElementException::new); 141 | menu.setDisplayed(false); 142 | return menu; 143 | } 144 | 145 | @Transactional(readOnly = true) 146 | public List findAll() { 147 | return menuRepository.findAll(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/JpaMenuGroupRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.UUID; 6 | 7 | public interface JpaMenuGroupRepository extends MenuGroupRepository, JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/JpaMenuRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.data.repository.query.Param; 6 | 7 | import java.util.List; 8 | import java.util.UUID; 9 | 10 | public interface JpaMenuRepository extends MenuRepository, JpaRepository { 11 | @Query("select m from Menu m join m.menuProducts mp where mp.product.id = :productId") 12 | @Override 13 | List findAllByProductId(@Param("productId") UUID productId); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/Menu.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.ForeignKey; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.ManyToOne; 10 | import jakarta.persistence.OneToMany; 11 | import jakarta.persistence.Table; 12 | import jakarta.persistence.Transient; 13 | 14 | import java.math.BigDecimal; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | @Table(name = "menu") 19 | @Entity 20 | public class Menu { 21 | @Column(name = "id", columnDefinition = "binary(16)") 22 | @Id 23 | private UUID id; 24 | 25 | @Column(name = "name", nullable = false) 26 | private String name; 27 | 28 | @Column(name = "price", nullable = false) 29 | private BigDecimal price; 30 | 31 | @ManyToOne(optional = false) 32 | @JoinColumn( 33 | name = "menu_group_id", 34 | columnDefinition = "binary(16)", 35 | foreignKey = @ForeignKey(name = "fk_menu_to_menu_group") 36 | ) 37 | private MenuGroup menuGroup; 38 | 39 | @Column(name = "displayed", nullable = false) 40 | private boolean displayed; 41 | 42 | @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) 43 | @JoinColumn( 44 | name = "menu_id", 45 | nullable = false, 46 | columnDefinition = "binary(16)", 47 | foreignKey = @ForeignKey(name = "fk_menu_product_to_menu") 48 | ) 49 | private List menuProducts; 50 | 51 | @Transient 52 | private UUID menuGroupId; 53 | 54 | public Menu() { 55 | } 56 | 57 | public UUID getId() { 58 | return id; 59 | } 60 | 61 | public void setId(final UUID id) { 62 | this.id = id; 63 | } 64 | 65 | public String getName() { 66 | return name; 67 | } 68 | 69 | public void setName(final String name) { 70 | this.name = name; 71 | } 72 | 73 | public BigDecimal getPrice() { 74 | return price; 75 | } 76 | 77 | public void setPrice(final BigDecimal price) { 78 | this.price = price; 79 | } 80 | 81 | public MenuGroup getMenuGroup() { 82 | return menuGroup; 83 | } 84 | 85 | public void setMenuGroup(final MenuGroup menuGroup) { 86 | this.menuGroup = menuGroup; 87 | } 88 | 89 | public boolean isDisplayed() { 90 | return displayed; 91 | } 92 | 93 | public void setDisplayed(final boolean displayed) { 94 | this.displayed = displayed; 95 | } 96 | 97 | public List getMenuProducts() { 98 | return menuProducts; 99 | } 100 | 101 | public void setMenuProducts(final List menuProducts) { 102 | this.menuProducts = menuProducts; 103 | } 104 | 105 | public UUID getMenuGroupId() { 106 | return menuGroupId; 107 | } 108 | 109 | public void setMenuGroupId(final UUID menuGroupId) { 110 | this.menuGroupId = menuGroupId; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/MenuGroup.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.Table; 7 | 8 | import java.util.UUID; 9 | 10 | @Table(name = "menu_group") 11 | @Entity 12 | public class MenuGroup { 13 | @Column(name = "id", columnDefinition = "binary(16)") 14 | @Id 15 | private UUID id; 16 | 17 | @Column(name = "name", nullable = false) 18 | private String name; 19 | 20 | public MenuGroup() { 21 | } 22 | 23 | public UUID getId() { 24 | return id; 25 | } 26 | 27 | public void setId(final UUID id) { 28 | this.id = id; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public void setName(final String name) { 36 | this.name = name; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/MenuGroupRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | public interface MenuGroupRepository { 8 | MenuGroup save(MenuGroup menuGroup); 9 | 10 | Optional findById(UUID id); 11 | 12 | List findAll(); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/MenuProduct.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.ForeignKey; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.JoinColumn; 10 | import jakarta.persistence.ManyToOne; 11 | import jakarta.persistence.Table; 12 | import jakarta.persistence.Transient; 13 | import kitchenpos.products.domain.Product; 14 | 15 | import java.util.UUID; 16 | 17 | @Table(name = "menu_product") 18 | @Entity 19 | public class MenuProduct { 20 | @Column(name = "seq") 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Id 23 | private Long seq; 24 | 25 | @ManyToOne(optional = false) 26 | @JoinColumn( 27 | name = "product_id", 28 | columnDefinition = "binary(16)", 29 | foreignKey = @ForeignKey(name = "fk_menu_product_to_product") 30 | ) 31 | private Product product; 32 | 33 | @Column(name = "quantity", nullable = false) 34 | private long quantity; 35 | 36 | @Transient 37 | private UUID productId; 38 | 39 | public MenuProduct() { 40 | } 41 | 42 | public Long getSeq() { 43 | return seq; 44 | } 45 | 46 | public void setSeq(final Long seq) { 47 | this.seq = seq; 48 | } 49 | 50 | public Product getProduct() { 51 | return product; 52 | } 53 | 54 | public void setProduct(final Product product) { 55 | this.product = product; 56 | } 57 | 58 | public long getQuantity() { 59 | return quantity; 60 | } 61 | 62 | public void setQuantity(final long quantity) { 63 | this.quantity = quantity; 64 | } 65 | 66 | public UUID getProductId() { 67 | return productId; 68 | } 69 | 70 | public void setProductId(final UUID productId) { 71 | this.productId = productId; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/domain/MenuRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | public interface MenuRepository { 8 | Menu save(Menu menu); 9 | 10 | Optional findById(UUID id); 11 | 12 | List findAll(); 13 | 14 | List findAllByIdIn(List ids); 15 | 16 | List findAllByProductId(UUID productId); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/ui/MenuGroupRestController.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.ui; 2 | 3 | import kitchenpos.menus.application.MenuGroupService; 4 | import kitchenpos.menus.domain.MenuGroup; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.net.URI; 13 | import java.util.List; 14 | 15 | @RequestMapping("/api/menu-groups") 16 | @RestController 17 | public class MenuGroupRestController { 18 | private final MenuGroupService menuGroupService; 19 | 20 | public MenuGroupRestController(final MenuGroupService menuGroupService) { 21 | this.menuGroupService = menuGroupService; 22 | } 23 | 24 | @PostMapping 25 | public ResponseEntity create(@RequestBody final MenuGroup request) { 26 | final MenuGroup response = menuGroupService.create(request); 27 | return ResponseEntity.created(URI.create("/api/menu-groups/" + response.getId())) 28 | .body(response); 29 | } 30 | 31 | @GetMapping 32 | public ResponseEntity> findAll() { 33 | return ResponseEntity.ok(menuGroupService.findAll()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/menus/ui/MenuRestController.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.ui; 2 | 3 | import kitchenpos.menus.application.MenuService; 4 | import kitchenpos.menus.domain.Menu; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.net.URI; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | @RequestMapping("/api/menus") 19 | @RestController 20 | public class MenuRestController { 21 | private final MenuService menuService; 22 | 23 | public MenuRestController(final MenuService menuService) { 24 | this.menuService = menuService; 25 | } 26 | 27 | @PostMapping 28 | public ResponseEntity create(@RequestBody final Menu request) { 29 | final Menu response = menuService.create(request); 30 | return ResponseEntity.created(URI.create("/api/menus/" + response.getId())) 31 | .body(response); 32 | } 33 | 34 | @PutMapping("/{menuId}/price") 35 | public ResponseEntity changePrice(@PathVariable final UUID menuId, @RequestBody final Menu request) { 36 | return ResponseEntity.ok(menuService.changePrice(menuId, request)); 37 | } 38 | 39 | @PutMapping("/{menuId}/display") 40 | public ResponseEntity display(@PathVariable final UUID menuId) { 41 | return ResponseEntity.ok(menuService.display(menuId)); 42 | } 43 | 44 | @PutMapping("/{menuId}/hide") 45 | public ResponseEntity hide(@PathVariable final UUID menuId) { 46 | return ResponseEntity.ok(menuService.hide(menuId)); 47 | } 48 | 49 | @GetMapping 50 | public ResponseEntity> findAll() { 51 | return ResponseEntity.ok(menuService.findAll()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/application/ProductService.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.application; 2 | 3 | import kitchenpos.menus.domain.Menu; 4 | import kitchenpos.menus.domain.MenuProduct; 5 | import kitchenpos.menus.domain.MenuRepository; 6 | import kitchenpos.products.domain.Product; 7 | import kitchenpos.products.domain.ProductRepository; 8 | import kitchenpos.products.infra.PurgomalumClient; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.List; 14 | import java.util.NoSuchElementException; 15 | import java.util.Objects; 16 | import java.util.UUID; 17 | 18 | @Service 19 | public class ProductService { 20 | private final ProductRepository productRepository; 21 | private final MenuRepository menuRepository; 22 | private final PurgomalumClient purgomalumClient; 23 | 24 | public ProductService( 25 | final ProductRepository productRepository, 26 | final MenuRepository menuRepository, 27 | final PurgomalumClient purgomalumClient 28 | ) { 29 | this.productRepository = productRepository; 30 | this.menuRepository = menuRepository; 31 | this.purgomalumClient = purgomalumClient; 32 | } 33 | 34 | @Transactional 35 | public Product create(final Product request) { 36 | final BigDecimal price = request.getPrice(); 37 | if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { 38 | throw new IllegalArgumentException(); 39 | } 40 | final String name = request.getName(); 41 | if (Objects.isNull(name) || purgomalumClient.containsProfanity(name)) { 42 | throw new IllegalArgumentException(); 43 | } 44 | final Product product = new Product(); 45 | product.setId(UUID.randomUUID()); 46 | product.setName(name); 47 | product.setPrice(price); 48 | return productRepository.save(product); 49 | } 50 | 51 | @Transactional 52 | public Product changePrice(final UUID productId, final Product request) { 53 | final BigDecimal price = request.getPrice(); 54 | if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { 55 | throw new IllegalArgumentException(); 56 | } 57 | final Product product = productRepository.findById(productId) 58 | .orElseThrow(NoSuchElementException::new); 59 | product.setPrice(price); 60 | final List menus = menuRepository.findAllByProductId(productId); 61 | for (final Menu menu : menus) { 62 | BigDecimal sum = BigDecimal.ZERO; 63 | for (final MenuProduct menuProduct : menu.getMenuProducts()) { 64 | sum = sum.add( 65 | menuProduct.getProduct() 66 | .getPrice() 67 | .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) 68 | ); 69 | } 70 | if (menu.getPrice().compareTo(sum) > 0) { 71 | menu.setDisplayed(false); 72 | } 73 | } 74 | return product; 75 | } 76 | 77 | @Transactional(readOnly = true) 78 | public List findAll() { 79 | return productRepository.findAll(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/domain/JpaProductRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.domain; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.UUID; 6 | 7 | public interface JpaProductRepository extends ProductRepository, JpaRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/domain/Product.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.Table; 7 | 8 | import java.math.BigDecimal; 9 | import java.util.UUID; 10 | 11 | @Table(name = "product") 12 | @Entity 13 | public class Product { 14 | @Column(name = "id", columnDefinition = "binary(16)") 15 | @Id 16 | private UUID id; 17 | 18 | @Column(name = "name", nullable = false) 19 | private String name; 20 | 21 | @Column(name = "price", nullable = false) 22 | private BigDecimal price; 23 | 24 | public Product() { 25 | } 26 | 27 | public UUID getId() { 28 | return id; 29 | } 30 | 31 | public void setId(final UUID id) { 32 | this.id = id; 33 | } 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public void setName(final String name) { 40 | this.name = name; 41 | } 42 | 43 | public BigDecimal getPrice() { 44 | return price; 45 | } 46 | 47 | public void setPrice(final BigDecimal price) { 48 | this.price = price; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/domain/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.UUID; 6 | 7 | public interface ProductRepository { 8 | Product save(Product product); 9 | 10 | Optional findById(UUID id); 11 | 12 | List findAll(); 13 | 14 | List findAllByIdIn(List ids); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.infra; 2 | 3 | import org.springframework.boot.web.client.RestTemplateBuilder; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.client.RestTemplate; 6 | import org.springframework.web.util.UriComponentsBuilder; 7 | 8 | import java.net.URI; 9 | 10 | @Component 11 | public class DefaultPurgomalumClient implements PurgomalumClient { 12 | private final RestTemplate restTemplate; 13 | 14 | public DefaultPurgomalumClient(final RestTemplateBuilder restTemplateBuilder) { 15 | this.restTemplate = restTemplateBuilder.build(); 16 | } 17 | 18 | @Override 19 | public boolean containsProfanity(final String text) { 20 | final URI url = UriComponentsBuilder.fromUriString("https://www.purgomalum.com/service/containsprofanity") 21 | .queryParam("text", text) 22 | .build() 23 | .toUri(); 24 | return Boolean.parseBoolean(restTemplate.getForObject(url, String.class)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/infra/PurgomalumClient.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.infra; 2 | 3 | public interface PurgomalumClient { 4 | boolean containsProfanity(String text); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/products/ui/ProductRestController.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.ui; 2 | 3 | import kitchenpos.products.application.ProductService; 4 | import kitchenpos.products.domain.Product; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.net.URI; 15 | import java.util.List; 16 | import java.util.UUID; 17 | 18 | @RequestMapping("/api/products") 19 | @RestController 20 | public class ProductRestController { 21 | private final ProductService productService; 22 | 23 | public ProductRestController(final ProductService productService) { 24 | this.productService = productService; 25 | } 26 | 27 | @PostMapping 28 | public ResponseEntity create(@RequestBody final Product request) { 29 | final Product response = productService.create(request); 30 | return ResponseEntity.created(URI.create("/api/products/" + response.getId())) 31 | .body(response); 32 | } 33 | 34 | @PutMapping("/{productId}/price") 35 | public ResponseEntity changePrice(@PathVariable final UUID productId, @RequestBody final Product request) { 36 | return ResponseEntity.ok(productService.changePrice(productId, request)); 37 | } 38 | 39 | @GetMapping 40 | public ResponseEntity> findAll() { 41 | return ResponseEntity.ok(productService.findAll()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/kitchenpos/takeoutorders/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/ddd-tactical-design/f712c32c9f727a24e41015467b47c8700caf930b/src/main/java/kitchenpos/takeoutorders/empty.txt -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.password=password 2 | spring.datasource.url=jdbc:mysql://localhost:33306/kitchenpos 3 | spring.datasource.username=user 4 | spring.flyway.enabled=true 5 | spring.jpa.hibernate.ddl-auto=validate 6 | spring.jpa.properties.hibernate.format_sql=true 7 | spring.jpa.show-sql=true 8 | logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 9 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__Initialize_project_tables.sql: -------------------------------------------------------------------------------- 1 | create table menu 2 | ( 3 | id binary(16) not null, 4 | displayed bit not null, 5 | name varchar(255) not null, 6 | price decimal(19, 2) not null, 7 | menu_group_id binary(16) not null, 8 | primary key (id) 9 | ) engine = InnoDB; 10 | 11 | create table menu_group 12 | ( 13 | id binary(16) not null, 14 | name varchar(255) not null, 15 | primary key (id) 16 | ) engine = InnoDB; 17 | 18 | create table menu_product 19 | ( 20 | seq bigint not null auto_increment, 21 | quantity bigint not null, 22 | product_id binary(16) not null, 23 | menu_id binary(16) not null, 24 | primary key (seq) 25 | ) engine = InnoDB; 26 | 27 | create table order_line_item 28 | ( 29 | seq bigint not null auto_increment, 30 | quantity bigint not null, 31 | menu_id binary(16) not null, 32 | order_id binary(16) not null, 33 | primary key (seq) 34 | ) engine = InnoDB; 35 | 36 | create table order_table 37 | ( 38 | id binary(16) not null, 39 | occupied bit not null, 40 | name varchar(255) not null, 41 | number_of_guests integer not null, 42 | primary key (id) 43 | ) engine = InnoDB; 44 | 45 | create table orders 46 | ( 47 | id binary(16) not null, 48 | delivery_address varchar(255), 49 | order_date_time datetime(6) not null, 50 | status varchar(255) not null, 51 | type varchar(255) not null, 52 | order_table_id binary(16), 53 | primary key (id) 54 | ) engine = InnoDB; 55 | 56 | create table product 57 | ( 58 | id binary(16) not null, 59 | name varchar(255) not null, 60 | price decimal(19, 2) not null, 61 | primary key (id) 62 | ) engine = InnoDB; 63 | 64 | alter table menu 65 | add constraint fk_menu_to_menu_group 66 | foreign key (menu_group_id) 67 | references menu_group (id); 68 | 69 | alter table menu_product 70 | add constraint fk_menu_product_to_product 71 | foreign key (product_id) 72 | references product (id); 73 | 74 | alter table menu_product 75 | add constraint fk_menu_product_to_menu 76 | foreign key (menu_id) 77 | references menu (id); 78 | 79 | alter table order_line_item 80 | add constraint fk_order_line_item_to_menu 81 | foreign key (menu_id) 82 | references menu (id); 83 | 84 | alter table order_line_item 85 | add constraint fk_order_line_item_to_orders 86 | foreign key (order_id) 87 | references orders (id); 88 | 89 | alter table orders 90 | add constraint fk_orders_to_order_table 91 | foreign key (order_table_id) 92 | references order_table (id); 93 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__Insert_default_data.sql: -------------------------------------------------------------------------------- 1 | insert into product (id, name, price) 2 | values (x'3b52824434f7406bbb7e690912f66b10', '후라이드', 16000); 3 | insert into product (id, name, price) 4 | values (x'c5ee925c3dbb4941b825021446f24446', '양념치킨', 16000); 5 | insert into product (id, name, price) 6 | values (x'625c6fc4145d408f8dd533c16ba26064', '반반치킨', 16000); 7 | insert into product (id, name, price) 8 | values (x'4721ee722ff3417fade3acd0a804605b', '통구이', 16000); 9 | insert into product (id, name, price) 10 | values (x'0ac16db71b024a87b9c1e7d8f226c48d', '간장치킨', 17000); 11 | insert into product (id, name, price) 12 | values (x'7de4b8affa0f4391aaa9c61ea9b40f83', '순살치킨', 17000); 13 | 14 | insert into menu_group (id, name) 15 | values (x'f1860abc2ea1411bbd4abaa44f0d5580', '두마리메뉴'); 16 | insert into menu_group (id, name) 17 | values (x'cbc75faefeb04bb18be2cb8ce5d8fded', '한마리메뉴'); 18 | insert into menu_group (id, name) 19 | values (x'5e9879b761124791a4cef22e94af8752', '순살파닭두마리메뉴'); 20 | insert into menu_group (id, name) 21 | values (x'd9bc21accc104593b5064a40e0170e02', '신메뉴'); 22 | 23 | insert into menu (id, displayed, name, price, menu_group_id) 24 | values (x'f59b1e1cb145440aaa6f6095a0e2d63b', true, '후라이드치킨', 16000, x'cbc75faefeb04bb18be2cb8ce5d8fded'); 25 | insert into menu (id, displayed, name, price, menu_group_id) 26 | values (x'e1254913860846aab23aa07c1dcbc648', true, '양념치킨', 16000, x'cbc75faefeb04bb18be2cb8ce5d8fded'); 27 | insert into menu (id, displayed, name, price, menu_group_id) 28 | values (x'191fa247b5f34b51b175e65db523f754', true, '반반치킨', 16000, x'cbc75faefeb04bb18be2cb8ce5d8fded'); 29 | insert into menu (id, displayed, name, price, menu_group_id) 30 | values (x'33e558df7d934622b50efcc4282cd184', true, '통구이', 16000, x'cbc75faefeb04bb18be2cb8ce5d8fded'); 31 | insert into menu (id, displayed, name, price, menu_group_id) 32 | values (x'b9c670b04ef5409083496868df1c7d62', true, '간장치킨', 17000, x'cbc75faefeb04bb18be2cb8ce5d8fded'); 33 | insert into menu (id, displayed, name, price, menu_group_id) 34 | values (x'a64af6cac34d4cd882fe454abf512d1f', true, '순살치킨', 17000, x'cbc75faefeb04bb18be2cb8ce5d8fded'); 35 | 36 | insert into menu_product (quantity, product_id, menu_id) 37 | values (1, x'3b52824434f7406bbb7e690912f66b10', x'f59b1e1cb145440aaa6f6095a0e2d63b'); 38 | insert into menu_product (quantity, product_id, menu_id) 39 | values (1, x'c5ee925c3dbb4941b825021446f24446', x'e1254913860846aab23aa07c1dcbc648'); 40 | insert into menu_product (quantity, product_id, menu_id) 41 | values (1, x'625c6fc4145d408f8dd533c16ba26064', x'191fa247b5f34b51b175e65db523f754'); 42 | insert into menu_product (quantity, product_id, menu_id) 43 | values (1, x'4721ee722ff3417fade3acd0a804605b', x'33e558df7d934622b50efcc4282cd184'); 44 | insert into menu_product (quantity, product_id, menu_id) 45 | values (1, x'0ac16db71b024a87b9c1e7d8f226c48d', x'b9c670b04ef5409083496868df1c7d62'); 46 | insert into menu_product (quantity, product_id, menu_id) 47 | values (1, x'7de4b8affa0f4391aaa9c61ea9b40f83', x'a64af6cac34d4cd882fe454abf512d1f'); 48 | 49 | insert into order_table (id, occupied, name, number_of_guests) 50 | values (x'8d71004329b6420e8452233f5a035520', false, '1번', 0); 51 | insert into order_table (id, occupied, name, number_of_guests) 52 | values (x'6ab59e8106eb441684e99faabc87c9ca', false, '2번', 0); 53 | insert into order_table (id, occupied, name, number_of_guests) 54 | values (x'ae92335ccd264626b7979e4ae8c4efbd', false, '3번', 0); 55 | insert into order_table (id, occupied, name, number_of_guests) 56 | values (x'a9858d4b80d0428881f48f41596a23fb', false, '4번', 0); 57 | insert into order_table (id, occupied, name, number_of_guests) 58 | values (x'3faec3ab5217405daaa2804f87697f84', false, '5번', 0); 59 | insert into order_table (id, occupied, name, number_of_guests) 60 | values (x'815b8395a2ad4e3589dc74c3b2191478', false, '6번', 0); 61 | insert into order_table (id, occupied, name, number_of_guests) 62 | values (x'7ce8b3a235454542ab9cb3d493bbd4fb', false, '7번', 0); 63 | insert into order_table (id, occupied, name, number_of_guests) 64 | values (x'7bdb1ffde36e4e2b94e3d2c14d391ef3', false, '8번', 0); 65 | 66 | insert into orders (id, delivery_address, order_date_time, status, type, order_table_id) 67 | values (x'69d78f383bff457cbb7226319c985fd8', '서울시 송파구 위례성대로 2', '2021-07-27', 'WAITING', 'DELIVERY', null); 68 | insert into orders (id, delivery_address, order_date_time, status, type, order_table_id) 69 | values (x'98da3d3859e04dacbbaeebf6560a43bd', null, '2021-07-27', 'COMPLETED', 'EAT_IN', 70 | x'8d71004329b6420e8452233f5a035520'); 71 | insert into orders (id, delivery_address, order_date_time, status, type, order_table_id) 72 | values (x'd7cc15b3e32c4bc8b440d3067b35522e', null, '2021-07-27', 'COMPLETED', 'EAT_IN', 73 | x'8d71004329b6420e8452233f5a035520'); 74 | 75 | insert into order_line_item (quantity, menu_id, order_id) 76 | values (1, x'f59b1e1cb145440aaa6f6095a0e2d63b', x'69d78f383bff457cbb7226319c985fd8'); 77 | insert into order_line_item (quantity, menu_id, order_id) 78 | values (1, x'f59b1e1cb145440aaa6f6095a0e2d63b', x'98da3d3859e04dacbbaeebf6560a43bd'); 79 | insert into order_line_item (quantity, menu_id, order_id) 80 | values (1, x'f59b1e1cb145440aaa6f6095a0e2d63b', x'd7cc15b3e32c4bc8b440d3067b35522e'); 81 | -------------------------------------------------------------------------------- /src/main/resources/static/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/ddd-tactical-design/f712c32c9f727a24e41015467b47c8700caf930b/src/main/resources/static/empty.txt -------------------------------------------------------------------------------- /src/main/resources/templates/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-step/ddd-tactical-design/f712c32c9f727a24e41015467b47c8700caf930b/src/main/resources/templates/empty.txt -------------------------------------------------------------------------------- /src/test/java/kitchenpos/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package kitchenpos; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ApplicationTest { 8 | @Test 9 | void contextLoads() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/Fixtures.java: -------------------------------------------------------------------------------- 1 | package kitchenpos; 2 | 3 | import kitchenpos.eatinorders.domain.Order; 4 | import kitchenpos.eatinorders.domain.OrderLineItem; 5 | import kitchenpos.eatinorders.domain.OrderStatus; 6 | import kitchenpos.eatinorders.domain.OrderTable; 7 | import kitchenpos.eatinorders.domain.OrderType; 8 | import kitchenpos.menus.domain.Menu; 9 | import kitchenpos.menus.domain.MenuGroup; 10 | import kitchenpos.menus.domain.MenuProduct; 11 | import kitchenpos.products.domain.Product; 12 | 13 | import java.math.BigDecimal; 14 | import java.time.LocalDateTime; 15 | import java.util.Arrays; 16 | import java.util.Random; 17 | import java.util.UUID; 18 | 19 | public class Fixtures { 20 | public static final UUID INVALID_ID = new UUID(0L, 0L); 21 | 22 | public static Menu menu() { 23 | return menu(19_000L, true, menuProduct()); 24 | } 25 | 26 | public static Menu menu(final long price, final MenuProduct... menuProducts) { 27 | return menu(price, false, menuProducts); 28 | } 29 | 30 | public static Menu menu(final long price, final boolean displayed, final MenuProduct... menuProducts) { 31 | final Menu menu = new Menu(); 32 | menu.setId(UUID.randomUUID()); 33 | menu.setName("후라이드+후라이드"); 34 | menu.setPrice(BigDecimal.valueOf(price)); 35 | menu.setMenuGroup(menuGroup()); 36 | menu.setDisplayed(displayed); 37 | menu.setMenuProducts(Arrays.asList(menuProducts)); 38 | return menu; 39 | } 40 | 41 | public static MenuGroup menuGroup() { 42 | return menuGroup("두마리메뉴"); 43 | } 44 | 45 | public static MenuGroup menuGroup(final String name) { 46 | final MenuGroup menuGroup = new MenuGroup(); 47 | menuGroup.setId(UUID.randomUUID()); 48 | menuGroup.setName(name); 49 | return menuGroup; 50 | } 51 | 52 | public static MenuProduct menuProduct() { 53 | final MenuProduct menuProduct = new MenuProduct(); 54 | menuProduct.setSeq(new Random().nextLong()); 55 | menuProduct.setProduct(product()); 56 | menuProduct.setQuantity(2L); 57 | return menuProduct; 58 | } 59 | 60 | public static MenuProduct menuProduct(final Product product, final long quantity) { 61 | final MenuProduct menuProduct = new MenuProduct(); 62 | menuProduct.setSeq(new Random().nextLong()); 63 | menuProduct.setProduct(product); 64 | menuProduct.setQuantity(quantity); 65 | return menuProduct; 66 | } 67 | 68 | public static Order order(final OrderStatus status, final String deliveryAddress) { 69 | final Order order = new Order(); 70 | order.setId(UUID.randomUUID()); 71 | order.setType(OrderType.DELIVERY); 72 | order.setStatus(status); 73 | order.setOrderDateTime(LocalDateTime.of(2020, 1, 1, 12, 0)); 74 | order.setOrderLineItems(Arrays.asList(orderLineItem())); 75 | order.setDeliveryAddress(deliveryAddress); 76 | return order; 77 | } 78 | 79 | public static Order order(final OrderStatus status) { 80 | final Order order = new Order(); 81 | order.setId(UUID.randomUUID()); 82 | order.setType(OrderType.TAKEOUT); 83 | order.setStatus(status); 84 | order.setOrderDateTime(LocalDateTime.of(2020, 1, 1, 12, 0)); 85 | order.setOrderLineItems(Arrays.asList(orderLineItem())); 86 | return order; 87 | } 88 | 89 | public static Order order(final OrderStatus status, final OrderTable orderTable) { 90 | final Order order = new Order(); 91 | order.setId(UUID.randomUUID()); 92 | order.setType(OrderType.EAT_IN); 93 | order.setStatus(status); 94 | order.setOrderDateTime(LocalDateTime.of(2020, 1, 1, 12, 0)); 95 | order.setOrderLineItems(Arrays.asList(orderLineItem())); 96 | order.setOrderTable(orderTable); 97 | return order; 98 | } 99 | 100 | public static OrderLineItem orderLineItem() { 101 | final OrderLineItem orderLineItem = new OrderLineItem(); 102 | orderLineItem.setSeq(new Random().nextLong()); 103 | orderLineItem.setMenu(menu()); 104 | return orderLineItem; 105 | } 106 | 107 | public static OrderTable orderTable() { 108 | return orderTable(false, 0); 109 | } 110 | 111 | public static OrderTable orderTable(final boolean occupied, final int numberOfGuests) { 112 | final OrderTable orderTable = new OrderTable(); 113 | orderTable.setId(UUID.randomUUID()); 114 | orderTable.setName("1번"); 115 | orderTable.setNumberOfGuests(numberOfGuests); 116 | orderTable.setOccupied(occupied); 117 | return orderTable; 118 | } 119 | 120 | public static Product product() { 121 | return product("후라이드", 16_000L); 122 | } 123 | 124 | public static Product product(final String name, final long price) { 125 | final Product product = new Product(); 126 | product.setId(UUID.randomUUID()); 127 | product.setName(name); 128 | product.setPrice(BigDecimal.valueOf(price)); 129 | return product; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/eatinorders/application/FakeKitchenridersClient.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.deliveryorders.infra.KitchenridersClient; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.UUID; 7 | 8 | public class FakeKitchenridersClient implements KitchenridersClient { 9 | private UUID orderId; 10 | private BigDecimal amount; 11 | private String deliveryAddress; 12 | 13 | @Override 14 | public void requestDelivery(final UUID orderId, final BigDecimal amount, final String deliveryAddress) { 15 | this.orderId = orderId; 16 | this.amount = amount; 17 | this.deliveryAddress = deliveryAddress; 18 | } 19 | 20 | public UUID getOrderId() { 21 | return orderId; 22 | } 23 | 24 | public BigDecimal getAmount() { 25 | return amount; 26 | } 27 | 28 | public String getDeliveryAddress() { 29 | return deliveryAddress; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/eatinorders/application/InMemoryOrderRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.eatinorders.domain.Order; 4 | import kitchenpos.eatinorders.domain.OrderRepository; 5 | import kitchenpos.eatinorders.domain.OrderStatus; 6 | import kitchenpos.eatinorders.domain.OrderTable; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | public class InMemoryOrderRepository implements OrderRepository { 16 | private final Map orders = new HashMap<>(); 17 | 18 | @Override 19 | public Order save(final Order order) { 20 | orders.put(order.getId(), order); 21 | return order; 22 | } 23 | 24 | @Override 25 | public Optional findById(final UUID id) { 26 | return Optional.ofNullable(orders.get(id)); 27 | } 28 | 29 | @Override 30 | public List findAll() { 31 | return new ArrayList<>(orders.values()); 32 | } 33 | 34 | @Override 35 | public boolean existsByOrderTableAndStatusNot(final OrderTable orderTable, final OrderStatus status) { 36 | return orders.values() 37 | .stream() 38 | .anyMatch(order -> order.getOrderTable().equals(orderTable) && order.getStatus() != status); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/eatinorders/application/InMemoryOrderTableRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.eatinorders.domain.OrderTable; 4 | import kitchenpos.eatinorders.domain.OrderTableRepository; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class InMemoryOrderTableRepository implements OrderTableRepository { 14 | private final Map orderTables = new HashMap<>(); 15 | 16 | @Override 17 | public OrderTable save(final OrderTable orderTable) { 18 | orderTables.put(orderTable.getId(), orderTable); 19 | return orderTable; 20 | } 21 | 22 | @Override 23 | public Optional findById(final UUID id) { 24 | return Optional.ofNullable(orderTables.get(id)); 25 | } 26 | 27 | @Override 28 | public List findAll() { 29 | return new ArrayList<>(orderTables.values()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/eatinorders/application/OrderServiceTest.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.eatinorders.domain.Order; 4 | import kitchenpos.eatinorders.domain.OrderLineItem; 5 | import kitchenpos.eatinorders.domain.OrderRepository; 6 | import kitchenpos.eatinorders.domain.OrderStatus; 7 | import kitchenpos.eatinorders.domain.OrderTable; 8 | import kitchenpos.eatinorders.domain.OrderTableRepository; 9 | import kitchenpos.eatinorders.domain.OrderType; 10 | import kitchenpos.menus.application.InMemoryMenuRepository; 11 | import kitchenpos.menus.domain.MenuRepository; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.params.ParameterizedTest; 16 | import org.junit.jupiter.params.provider.Arguments; 17 | import org.junit.jupiter.params.provider.EnumSource; 18 | import org.junit.jupiter.params.provider.MethodSource; 19 | import org.junit.jupiter.params.provider.NullAndEmptySource; 20 | import org.junit.jupiter.params.provider.NullSource; 21 | import org.junit.jupiter.params.provider.ValueSource; 22 | 23 | import java.math.BigDecimal; 24 | import java.util.Arrays; 25 | import java.util.Collections; 26 | import java.util.List; 27 | import java.util.Random; 28 | import java.util.UUID; 29 | 30 | import static kitchenpos.Fixtures.INVALID_ID; 31 | import static kitchenpos.Fixtures.menu; 32 | import static kitchenpos.Fixtures.menuProduct; 33 | import static kitchenpos.Fixtures.order; 34 | import static kitchenpos.Fixtures.orderTable; 35 | import static org.assertj.core.api.Assertions.assertThat; 36 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 37 | import static org.junit.jupiter.api.Assertions.assertAll; 38 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 39 | 40 | class OrderServiceTest { 41 | private OrderRepository orderRepository; 42 | private MenuRepository menuRepository; 43 | private OrderTableRepository orderTableRepository; 44 | private FakeKitchenridersClient kitchenridersClient; 45 | private OrderService orderService; 46 | 47 | @BeforeEach 48 | void setUp() { 49 | orderRepository = new InMemoryOrderRepository(); 50 | menuRepository = new InMemoryMenuRepository(); 51 | orderTableRepository = new InMemoryOrderTableRepository(); 52 | kitchenridersClient = new FakeKitchenridersClient(); 53 | orderService = new OrderService(orderRepository, menuRepository, orderTableRepository, kitchenridersClient); 54 | } 55 | 56 | @DisplayName("1개 이상의 등록된 메뉴로 배달 주문을 등록할 수 있다.") 57 | @Test 58 | void createDeliveryOrder() { 59 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 60 | final Order expected = createOrderRequest( 61 | OrderType.DELIVERY, "서울시 송파구 위례성대로 2", createOrderLineItemRequest(menuId, 19_000L, 3L) 62 | ); 63 | final Order actual = orderService.create(expected); 64 | assertThat(actual).isNotNull(); 65 | assertAll( 66 | () -> assertThat(actual.getId()).isNotNull(), 67 | () -> assertThat(actual.getType()).isEqualTo(expected.getType()), 68 | () -> assertThat(actual.getStatus()).isEqualTo(OrderStatus.WAITING), 69 | () -> assertThat(actual.getOrderDateTime()).isNotNull(), 70 | () -> assertThat(actual.getOrderLineItems()).hasSize(1), 71 | () -> assertThat(actual.getDeliveryAddress()).isEqualTo(expected.getDeliveryAddress()) 72 | ); 73 | } 74 | 75 | @DisplayName("1개 이상의 등록된 메뉴로 포장 주문을 등록할 수 있다.") 76 | @Test 77 | void createTakeoutOrder() { 78 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 79 | final Order expected = createOrderRequest(OrderType.TAKEOUT, createOrderLineItemRequest(menuId, 19_000L, 3L)); 80 | final Order actual = orderService.create(expected); 81 | assertThat(actual).isNotNull(); 82 | assertAll( 83 | () -> assertThat(actual.getId()).isNotNull(), 84 | () -> assertThat(actual.getType()).isEqualTo(expected.getType()), 85 | () -> assertThat(actual.getStatus()).isEqualTo(OrderStatus.WAITING), 86 | () -> assertThat(actual.getOrderDateTime()).isNotNull(), 87 | () -> assertThat(actual.getOrderLineItems()).hasSize(1) 88 | ); 89 | } 90 | 91 | @DisplayName("1개 이상의 등록된 메뉴로 매장 주문을 등록할 수 있다.") 92 | @Test 93 | void createEatInOrder() { 94 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 95 | final UUID orderTableId = orderTableRepository.save(orderTable(true, 4)).getId(); 96 | final Order expected = createOrderRequest(OrderType.EAT_IN, orderTableId, createOrderLineItemRequest(menuId, 19_000L, 3L)); 97 | final Order actual = orderService.create(expected); 98 | assertThat(actual).isNotNull(); 99 | assertAll( 100 | () -> assertThat(actual.getId()).isNotNull(), 101 | () -> assertThat(actual.getType()).isEqualTo(expected.getType()), 102 | () -> assertThat(actual.getStatus()).isEqualTo(OrderStatus.WAITING), 103 | () -> assertThat(actual.getOrderDateTime()).isNotNull(), 104 | () -> assertThat(actual.getOrderLineItems()).hasSize(1), 105 | () -> assertThat(actual.getOrderTable().getId()).isEqualTo(expected.getOrderTableId()) 106 | ); 107 | } 108 | 109 | @DisplayName("주문 유형이 올바르지 않으면 등록할 수 없다.") 110 | @NullSource 111 | @ParameterizedTest 112 | void create(final OrderType type) { 113 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 114 | final Order expected = createOrderRequest(type, createOrderLineItemRequest(menuId, 19_000L, 3L)); 115 | assertThatThrownBy(() -> orderService.create(expected)) 116 | .isInstanceOf(IllegalArgumentException.class); 117 | } 118 | 119 | @DisplayName("메뉴가 없으면 등록할 수 없다.") 120 | @MethodSource("orderLineItems") 121 | @ParameterizedTest 122 | void create(final List orderLineItems) { 123 | final Order expected = createOrderRequest(OrderType.TAKEOUT, orderLineItems); 124 | assertThatThrownBy(() -> orderService.create(expected)) 125 | .isInstanceOf(IllegalArgumentException.class); 126 | } 127 | 128 | private static List orderLineItems() { 129 | return Arrays.asList( 130 | null, 131 | Arguments.of(Collections.emptyList()), 132 | Arguments.of(Arrays.asList(createOrderLineItemRequest(INVALID_ID, 19_000L, 3L))) 133 | ); 134 | } 135 | 136 | @DisplayName("매장 주문은 주문 항목의 수량이 0 미만일 수 있다.") 137 | @ValueSource(longs = -1L) 138 | @ParameterizedTest 139 | void createEatInOrder(final long quantity) { 140 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 141 | final UUID orderTableId = orderTableRepository.save(orderTable(true, 4)).getId(); 142 | final Order expected = createOrderRequest( 143 | OrderType.EAT_IN, orderTableId, createOrderLineItemRequest(menuId, 19_000L, quantity) 144 | ); 145 | assertDoesNotThrow(() -> orderService.create(expected)); 146 | } 147 | 148 | @DisplayName("매장 주문을 제외한 주문의 경우 주문 항목의 수량은 0 이상이어야 한다.") 149 | @ValueSource(longs = -1L) 150 | @ParameterizedTest 151 | void createWithoutEatInOrder(final long quantity) { 152 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 153 | final Order expected = createOrderRequest( 154 | OrderType.TAKEOUT, createOrderLineItemRequest(menuId, 19_000L, quantity) 155 | ); 156 | assertThatThrownBy(() -> orderService.create(expected)) 157 | .isInstanceOf(IllegalArgumentException.class); 158 | } 159 | 160 | @DisplayName("배달 주소가 올바르지 않으면 배달 주문을 등록할 수 없다.") 161 | @NullAndEmptySource 162 | @ParameterizedTest 163 | void create(final String deliveryAddress) { 164 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 165 | final Order expected = createOrderRequest( 166 | OrderType.DELIVERY, deliveryAddress, createOrderLineItemRequest(menuId, 19_000L, 3L) 167 | ); 168 | assertThatThrownBy(() -> orderService.create(expected)) 169 | .isInstanceOf(IllegalArgumentException.class); 170 | } 171 | 172 | @DisplayName("빈 테이블에는 매장 주문을 등록할 수 없다.") 173 | @Test 174 | void createEmptyTableEatInOrder() { 175 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 176 | final UUID orderTableId = orderTableRepository.save(orderTable(false, 0)).getId(); 177 | final Order expected = createOrderRequest( 178 | OrderType.EAT_IN, orderTableId, createOrderLineItemRequest(menuId, 19_000L, 3L) 179 | ); 180 | assertThatThrownBy(() -> orderService.create(expected)) 181 | .isInstanceOf(IllegalStateException.class); 182 | } 183 | 184 | @DisplayName("숨겨진 메뉴는 주문할 수 없다.") 185 | @Test 186 | void createNotDisplayedMenuOrder() { 187 | final UUID menuId = menuRepository.save(menu(19_000L, false, menuProduct())).getId(); 188 | final Order expected = createOrderRequest(OrderType.TAKEOUT, createOrderLineItemRequest(menuId, 19_000L, 3L)); 189 | assertThatThrownBy(() -> orderService.create(expected)) 190 | .isInstanceOf(IllegalStateException.class); 191 | } 192 | 193 | @DisplayName("주문한 메뉴의 가격은 실제 메뉴 가격과 일치해야 한다.") 194 | @Test 195 | void createNotMatchedMenuPriceOrder() { 196 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct())).getId(); 197 | final Order expected = createOrderRequest(OrderType.TAKEOUT, createOrderLineItemRequest(menuId, 16_000L, 3L)); 198 | assertThatThrownBy(() -> orderService.create(expected)) 199 | .isInstanceOf(IllegalArgumentException.class); 200 | } 201 | 202 | @DisplayName("주문을 접수한다.") 203 | @Test 204 | void accept() { 205 | final UUID orderId = orderRepository.save(order(OrderStatus.WAITING, orderTable(true, 4))).getId(); 206 | final Order actual = orderService.accept(orderId); 207 | assertThat(actual.getStatus()).isEqualTo(OrderStatus.ACCEPTED); 208 | } 209 | 210 | @DisplayName("접수 대기 중인 주문만 접수할 수 있다.") 211 | @EnumSource(value = OrderStatus.class, names = "WAITING", mode = EnumSource.Mode.EXCLUDE) 212 | @ParameterizedTest 213 | void accept(final OrderStatus status) { 214 | final UUID orderId = orderRepository.save(order(status, orderTable(true, 4))).getId(); 215 | assertThatThrownBy(() -> orderService.accept(orderId)) 216 | .isInstanceOf(IllegalStateException.class); 217 | } 218 | 219 | @DisplayName("배달 주문을 접수되면 배달 대행사를 호출한다.") 220 | @Test 221 | void acceptDeliveryOrder() { 222 | final UUID orderId = orderRepository.save(order(OrderStatus.WAITING, "서울시 송파구 위례성대로 2")).getId(); 223 | final Order actual = orderService.accept(orderId); 224 | assertAll( 225 | () -> assertThat(actual.getStatus()).isEqualTo(OrderStatus.ACCEPTED), 226 | () -> assertThat(kitchenridersClient.getOrderId()).isEqualTo(orderId), 227 | () -> assertThat(kitchenridersClient.getDeliveryAddress()).isEqualTo("서울시 송파구 위례성대로 2") 228 | ); 229 | } 230 | 231 | @DisplayName("주문을 서빙한다.") 232 | @Test 233 | void serve() { 234 | final UUID orderId = orderRepository.save(order(OrderStatus.ACCEPTED)).getId(); 235 | final Order actual = orderService.serve(orderId); 236 | assertThat(actual.getStatus()).isEqualTo(OrderStatus.SERVED); 237 | } 238 | 239 | @DisplayName("접수된 주문만 서빙할 수 있다.") 240 | @EnumSource(value = OrderStatus.class, names = "ACCEPTED", mode = EnumSource.Mode.EXCLUDE) 241 | @ParameterizedTest 242 | void serve(final OrderStatus status) { 243 | final UUID orderId = orderRepository.save(order(status)).getId(); 244 | assertThatThrownBy(() -> orderService.serve(orderId)) 245 | .isInstanceOf(IllegalStateException.class); 246 | } 247 | 248 | @DisplayName("주문을 배달한다.") 249 | @Test 250 | void startDelivery() { 251 | final UUID orderId = orderRepository.save(order(OrderStatus.SERVED, "서울시 송파구 위례성대로 2")).getId(); 252 | final Order actual = orderService.startDelivery(orderId); 253 | assertThat(actual.getStatus()).isEqualTo(OrderStatus.DELIVERING); 254 | } 255 | 256 | @DisplayName("배달 주문만 배달할 수 있다.") 257 | @Test 258 | void startDeliveryWithoutDeliveryOrder() { 259 | final UUID orderId = orderRepository.save(order(OrderStatus.SERVED)).getId(); 260 | assertThatThrownBy(() -> orderService.startDelivery(orderId)) 261 | .isInstanceOf(IllegalStateException.class); 262 | } 263 | 264 | @DisplayName("서빙된 주문만 배달할 수 있다.") 265 | @EnumSource(value = OrderStatus.class, names = "SERVED", mode = EnumSource.Mode.EXCLUDE) 266 | @ParameterizedTest 267 | void startDelivery(final OrderStatus status) { 268 | final UUID orderId = orderRepository.save(order(status, "서울시 송파구 위례성대로 2")).getId(); 269 | assertThatThrownBy(() -> orderService.startDelivery(orderId)) 270 | .isInstanceOf(IllegalStateException.class); 271 | } 272 | 273 | @DisplayName("주문을 배달 완료한다.") 274 | @Test 275 | void completeDelivery() { 276 | final UUID orderId = orderRepository.save(order(OrderStatus.DELIVERING, "서울시 송파구 위례성대로 2")).getId(); 277 | final Order actual = orderService.completeDelivery(orderId); 278 | assertThat(actual.getStatus()).isEqualTo(OrderStatus.DELIVERED); 279 | } 280 | 281 | @DisplayName("배달 중인 주문만 배달 완료할 수 있다.") 282 | @EnumSource(value = OrderStatus.class, names = "DELIVERING", mode = EnumSource.Mode.EXCLUDE) 283 | @ParameterizedTest 284 | void completeDelivery(final OrderStatus status) { 285 | final UUID orderId = orderRepository.save(order(status, "서울시 송파구 위례성대로 2")).getId(); 286 | assertThatThrownBy(() -> orderService.completeDelivery(orderId)) 287 | .isInstanceOf(IllegalStateException.class); 288 | } 289 | 290 | @DisplayName("주문을 완료한다.") 291 | @Test 292 | void complete() { 293 | final Order expected = orderRepository.save(order(OrderStatus.DELIVERED, "서울시 송파구 위례성대로 2")); 294 | final Order actual = orderService.complete(expected.getId()); 295 | assertThat(actual.getStatus()).isEqualTo(OrderStatus.COMPLETED); 296 | } 297 | 298 | @DisplayName("배달 주문의 경우 배달 완료된 주문만 완료할 수 있다.") 299 | @EnumSource(value = OrderStatus.class, names = "DELIVERED", mode = EnumSource.Mode.EXCLUDE) 300 | @ParameterizedTest 301 | void completeDeliveryOrder(final OrderStatus status) { 302 | final UUID orderId = orderRepository.save(order(status, "서울시 송파구 위례성대로 2")).getId(); 303 | assertThatThrownBy(() -> orderService.complete(orderId)) 304 | .isInstanceOf(IllegalStateException.class); 305 | } 306 | 307 | @DisplayName("포장 및 매장 주문의 경우 서빙된 주문만 완료할 수 있다.") 308 | @EnumSource(value = OrderStatus.class, names = "SERVED", mode = EnumSource.Mode.EXCLUDE) 309 | @ParameterizedTest 310 | void completeTakeoutAndEatInOrder(final OrderStatus status) { 311 | final UUID orderId = orderRepository.save(order(status)).getId(); 312 | assertThatThrownBy(() -> orderService.complete(orderId)) 313 | .isInstanceOf(IllegalStateException.class); 314 | } 315 | 316 | @DisplayName("주문 테이블의 모든 매장 주문이 완료되면 빈 테이블로 설정한다.") 317 | @Test 318 | void completeEatInOrder() { 319 | final OrderTable orderTable = orderTableRepository.save(orderTable(true, 4)); 320 | final Order expected = orderRepository.save(order(OrderStatus.SERVED, orderTable)); 321 | final Order actual = orderService.complete(expected.getId()); 322 | assertAll( 323 | () -> assertThat(actual.getStatus()).isEqualTo(OrderStatus.COMPLETED), 324 | () -> assertThat(orderTableRepository.findById(orderTable.getId()).get().isOccupied()).isFalse(), 325 | () -> assertThat(orderTableRepository.findById(orderTable.getId()).get().getNumberOfGuests()).isEqualTo(0) 326 | ); 327 | } 328 | 329 | @DisplayName("완료되지 않은 매장 주문이 있는 주문 테이블은 빈 테이블로 설정하지 않는다.") 330 | @Test 331 | void completeNotTable() { 332 | final OrderTable orderTable = orderTableRepository.save(orderTable(true, 4)); 333 | orderRepository.save(order(OrderStatus.ACCEPTED, orderTable)); 334 | final Order expected = orderRepository.save(order(OrderStatus.SERVED, orderTable)); 335 | final Order actual = orderService.complete(expected.getId()); 336 | assertAll( 337 | () -> assertThat(actual.getStatus()).isEqualTo(OrderStatus.COMPLETED), 338 | () -> assertThat(orderTableRepository.findById(orderTable.getId()).get().isOccupied()).isTrue(), 339 | () -> assertThat(orderTableRepository.findById(orderTable.getId()).get().getNumberOfGuests()).isEqualTo(4) 340 | ); 341 | } 342 | 343 | @DisplayName("주문의 목록을 조회할 수 있다.") 344 | @Test 345 | void findAll() { 346 | final OrderTable orderTable = orderTableRepository.save(orderTable(true, 4)); 347 | orderRepository.save(order(OrderStatus.SERVED, orderTable)); 348 | orderRepository.save(order(OrderStatus.DELIVERED, "서울시 송파구 위례성대로 2")); 349 | final List actual = orderService.findAll(); 350 | assertThat(actual).hasSize(2); 351 | } 352 | 353 | private Order createOrderRequest( 354 | final OrderType type, 355 | final String deliveryAddress, 356 | final OrderLineItem... orderLineItems 357 | ) { 358 | final Order order = new Order(); 359 | order.setType(type); 360 | order.setDeliveryAddress(deliveryAddress); 361 | order.setOrderLineItems(Arrays.asList(orderLineItems)); 362 | return order; 363 | } 364 | 365 | private Order createOrderRequest(final OrderType orderType, final OrderLineItem... orderLineItems) { 366 | return createOrderRequest(orderType, Arrays.asList(orderLineItems)); 367 | } 368 | 369 | private Order createOrderRequest(final OrderType orderType, final List orderLineItems) { 370 | final Order order = new Order(); 371 | order.setType(orderType); 372 | order.setOrderLineItems(orderLineItems); 373 | return order; 374 | } 375 | 376 | private Order createOrderRequest( 377 | final OrderType type, 378 | final UUID orderTableId, 379 | final OrderLineItem... orderLineItems 380 | ) { 381 | final Order order = new Order(); 382 | order.setType(type); 383 | order.setOrderTableId(orderTableId); 384 | order.setOrderLineItems(Arrays.asList(orderLineItems)); 385 | return order; 386 | } 387 | 388 | private static OrderLineItem createOrderLineItemRequest(final UUID menuId, final long price, final long quantity) { 389 | final OrderLineItem orderLineItem = new OrderLineItem(); 390 | orderLineItem.setSeq(new Random().nextLong()); 391 | orderLineItem.setMenuId(menuId); 392 | orderLineItem.setPrice(BigDecimal.valueOf(price)); 393 | orderLineItem.setQuantity(quantity); 394 | return orderLineItem; 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/eatinorders/application/OrderTableServiceTest.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.eatinorders.application; 2 | 3 | import kitchenpos.eatinorders.domain.OrderRepository; 4 | import kitchenpos.eatinorders.domain.OrderStatus; 5 | import kitchenpos.eatinorders.domain.OrderTable; 6 | import kitchenpos.eatinorders.domain.OrderTableRepository; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.params.ParameterizedTest; 11 | import org.junit.jupiter.params.provider.NullAndEmptySource; 12 | import org.junit.jupiter.params.provider.ValueSource; 13 | 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | import static kitchenpos.Fixtures.order; 18 | import static kitchenpos.Fixtures.orderTable; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | import static org.junit.jupiter.api.Assertions.assertAll; 22 | 23 | class OrderTableServiceTest { 24 | private OrderTableRepository orderTableRepository; 25 | private OrderRepository orderRepository; 26 | private OrderTableService orderTableService; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | orderTableRepository = new InMemoryOrderTableRepository(); 31 | orderRepository = new InMemoryOrderRepository(); 32 | orderTableService = new OrderTableService(orderTableRepository, orderRepository); 33 | } 34 | 35 | @DisplayName("주문 테이블을 등록할 수 있다.") 36 | @Test 37 | void create() { 38 | final OrderTable expected = createOrderTableRequest("1번"); 39 | final OrderTable actual = orderTableService.create(expected); 40 | assertThat(actual).isNotNull(); 41 | assertAll( 42 | () -> assertThat(actual.getId()).isNotNull(), 43 | () -> assertThat(actual.getName()).isEqualTo(expected.getName()), 44 | () -> assertThat(actual.getNumberOfGuests()).isZero(), 45 | () -> assertThat(actual.isOccupied()).isFalse() 46 | ); 47 | } 48 | 49 | @DisplayName("주문 테이블의 이름이 올바르지 않으면 등록할 수 없다.") 50 | @NullAndEmptySource 51 | @ParameterizedTest 52 | void create(final String name) { 53 | final OrderTable expected = createOrderTableRequest(name); 54 | assertThatThrownBy(() -> orderTableService.create(expected)) 55 | .isInstanceOf(IllegalArgumentException.class); 56 | } 57 | 58 | @DisplayName("빈 테이블을 해지할 수 있다.") 59 | @Test 60 | void sit() { 61 | final UUID orderTableId = orderTableRepository.save(orderTable(false, 0)).getId(); 62 | final OrderTable actual = orderTableService.sit(orderTableId); 63 | assertThat(actual.isOccupied()).isTrue(); 64 | } 65 | 66 | @DisplayName("빈 테이블로 설정할 수 있다.") 67 | @Test 68 | void clear() { 69 | final UUID orderTableId = orderTableRepository.save(orderTable(true, 4)).getId(); 70 | final OrderTable actual = orderTableService.clear(orderTableId); 71 | assertAll( 72 | () -> assertThat(actual.getNumberOfGuests()).isZero(), 73 | () -> assertThat(actual.isOccupied()).isFalse() 74 | ); 75 | } 76 | 77 | @DisplayName("완료되지 않은 주문이 있는 주문 테이블은 빈 테이블로 설정할 수 없다.") 78 | @Test 79 | void clearWithUncompletedOrders() { 80 | final OrderTable orderTable = orderTableRepository.save(orderTable(true, 4)); 81 | final UUID orderTableId = orderTable.getId(); 82 | orderRepository.save(order(OrderStatus.ACCEPTED, orderTable)); 83 | assertThatThrownBy(() -> orderTableService.clear(orderTableId)) 84 | .isInstanceOf(IllegalStateException.class); 85 | } 86 | 87 | @DisplayName("방문한 손님 수를 변경할 수 있다.") 88 | @Test 89 | void changeNumberOfGuests() { 90 | final UUID orderTableId = orderTableRepository.save(orderTable(true, 0)).getId(); 91 | final OrderTable expected = changeNumberOfGuestsRequest(4); 92 | final OrderTable actual = orderTableService.changeNumberOfGuests(orderTableId, expected); 93 | assertThat(actual.getNumberOfGuests()).isEqualTo(4); 94 | } 95 | 96 | @DisplayName("방문한 손님 수가 올바르지 않으면 변경할 수 없다.") 97 | @ValueSource(ints = -1) 98 | @ParameterizedTest 99 | void changeNumberOfGuests(final int numberOfGuests) { 100 | final UUID orderTableId = orderTableRepository.save(orderTable(true, 0)).getId(); 101 | final OrderTable expected = changeNumberOfGuestsRequest(numberOfGuests); 102 | assertThatThrownBy(() -> orderTableService.changeNumberOfGuests(orderTableId, expected)) 103 | .isInstanceOf(IllegalArgumentException.class); 104 | } 105 | 106 | @DisplayName("빈 테이블은 방문한 손님 수를 변경할 수 없다.") 107 | @Test 108 | void changeNumberOfGuestsInEmptyTable() { 109 | final UUID orderTableId = orderTableRepository.save(orderTable(false, 0)).getId(); 110 | final OrderTable expected = changeNumberOfGuestsRequest(4); 111 | assertThatThrownBy(() -> orderTableService.changeNumberOfGuests(orderTableId, expected)) 112 | .isInstanceOf(IllegalStateException.class); 113 | } 114 | 115 | @DisplayName("주문 테이블의 목록을 조회할 수 있다.") 116 | @Test 117 | void findAll() { 118 | orderTableRepository.save(orderTable()); 119 | final List actual = orderTableService.findAll(); 120 | assertThat(actual).hasSize(1); 121 | } 122 | 123 | private OrderTable createOrderTableRequest(final String name) { 124 | final OrderTable orderTable = new OrderTable(); 125 | orderTable.setName(name); 126 | return orderTable; 127 | } 128 | 129 | private OrderTable changeNumberOfGuestsRequest(final int numberOfGuests) { 130 | final OrderTable orderTable = new OrderTable(); 131 | orderTable.setNumberOfGuests(numberOfGuests); 132 | return orderTable; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/menus/application/InMemoryMenuGroupRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.application; 2 | 3 | import kitchenpos.menus.domain.MenuGroup; 4 | import kitchenpos.menus.domain.MenuGroupRepository; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class InMemoryMenuGroupRepository implements MenuGroupRepository { 14 | private final Map menuGroups = new HashMap<>(); 15 | 16 | @Override 17 | public MenuGroup save(final MenuGroup menuGroup) { 18 | menuGroups.put(menuGroup.getId(), menuGroup); 19 | return menuGroup; 20 | } 21 | 22 | @Override 23 | public Optional findById(final UUID id) { 24 | return Optional.ofNullable(menuGroups.get(id)); 25 | } 26 | 27 | @Override 28 | public List findAll() { 29 | return new ArrayList<>(menuGroups.values()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/menus/application/InMemoryMenuRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.application; 2 | 3 | import kitchenpos.menus.domain.Menu; 4 | import kitchenpos.menus.domain.MenuRepository; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class InMemoryMenuRepository implements MenuRepository { 14 | private final Map menus = new HashMap<>(); 15 | 16 | @Override 17 | public Menu save(final Menu menu) { 18 | menus.put(menu.getId(), menu); 19 | return menu; 20 | } 21 | 22 | @Override 23 | public Optional findById(final UUID id) { 24 | return Optional.ofNullable(menus.get(id)); 25 | } 26 | 27 | @Override 28 | public List findAll() { 29 | return new ArrayList<>(menus.values()); 30 | } 31 | 32 | @Override 33 | public List findAllByIdIn(final List ids) { 34 | return menus.values() 35 | .stream() 36 | .filter(menu -> ids.contains(menu.getId())) 37 | .toList(); 38 | } 39 | 40 | @Override 41 | public List findAllByProductId(final UUID productId) { 42 | return menus.values() 43 | .stream() 44 | .filter(menu -> menu.getMenuProducts().stream().anyMatch(menuProduct -> menuProduct.getProduct().getId().equals(productId))) 45 | .toList(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/menus/application/MenuGroupServiceTest.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.application; 2 | 3 | import kitchenpos.menus.domain.MenuGroup; 4 | import kitchenpos.menus.domain.MenuGroupRepository; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.NullAndEmptySource; 10 | 11 | import java.util.List; 12 | 13 | import static kitchenpos.Fixtures.menuGroup; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 16 | import static org.junit.jupiter.api.Assertions.assertAll; 17 | 18 | class MenuGroupServiceTest { 19 | private MenuGroupRepository menuGroupRepository; 20 | private MenuGroupService menuGroupService; 21 | 22 | @BeforeEach 23 | void setUp() { 24 | menuGroupRepository = new InMemoryMenuGroupRepository(); 25 | menuGroupService = new MenuGroupService(menuGroupRepository); 26 | } 27 | 28 | @DisplayName("메뉴 그룹을 등록할 수 있다.") 29 | @Test 30 | void create() { 31 | final MenuGroup expected = createMenuGroupRequest("두마리메뉴"); 32 | final MenuGroup actual = menuGroupService.create(expected); 33 | assertThat(actual).isNotNull(); 34 | assertAll( 35 | () -> assertThat(actual.getId()).isNotNull(), 36 | () -> assertThat(actual.getName()).isEqualTo(expected.getName()) 37 | ); 38 | } 39 | 40 | @DisplayName("메뉴 그룹의 이름이 올바르지 않으면 등록할 수 없다.") 41 | @NullAndEmptySource 42 | @ParameterizedTest 43 | void create(final String name) { 44 | final MenuGroup expected = createMenuGroupRequest(name); 45 | assertThatThrownBy(() -> menuGroupService.create(expected)) 46 | .isInstanceOf(IllegalArgumentException.class); 47 | } 48 | 49 | @DisplayName("메뉴 그룹의 목록을 조회할 수 있다.") 50 | @Test 51 | void findAll() { 52 | menuGroupRepository.save(menuGroup("두마리메뉴")); 53 | final List actual = menuGroupService.findAll(); 54 | assertThat(actual).hasSize(1); 55 | } 56 | 57 | private MenuGroup createMenuGroupRequest(final String name) { 58 | final MenuGroup menuGroup = new MenuGroup(); 59 | menuGroup.setName(name); 60 | return menuGroup; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/menus/application/MenuServiceTest.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.menus.application; 2 | 3 | import kitchenpos.menus.domain.Menu; 4 | import kitchenpos.menus.domain.MenuGroupRepository; 5 | import kitchenpos.menus.domain.MenuProduct; 6 | import kitchenpos.menus.domain.MenuRepository; 7 | import kitchenpos.products.application.FakePurgomalumClient; 8 | import kitchenpos.products.application.InMemoryProductRepository; 9 | import kitchenpos.products.domain.Product; 10 | import kitchenpos.products.domain.ProductRepository; 11 | import kitchenpos.products.infra.PurgomalumClient; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.params.ParameterizedTest; 16 | import org.junit.jupiter.params.provider.Arguments; 17 | import org.junit.jupiter.params.provider.MethodSource; 18 | import org.junit.jupiter.params.provider.NullSource; 19 | import org.junit.jupiter.params.provider.ValueSource; 20 | 21 | import java.math.BigDecimal; 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | import java.util.List; 25 | import java.util.NoSuchElementException; 26 | import java.util.UUID; 27 | 28 | import static kitchenpos.Fixtures.INVALID_ID; 29 | import static kitchenpos.Fixtures.menu; 30 | import static kitchenpos.Fixtures.menuGroup; 31 | import static kitchenpos.Fixtures.menuProduct; 32 | import static kitchenpos.Fixtures.product; 33 | import static org.assertj.core.api.Assertions.assertThat; 34 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 35 | import static org.junit.jupiter.api.Assertions.assertAll; 36 | 37 | class MenuServiceTest { 38 | private MenuRepository menuRepository; 39 | private MenuGroupRepository menuGroupRepository; 40 | private ProductRepository productRepository; 41 | private PurgomalumClient purgomalumClient; 42 | private MenuService menuService; 43 | private UUID menuGroupId; 44 | private Product product; 45 | 46 | @BeforeEach 47 | void setUp() { 48 | menuRepository = new InMemoryMenuRepository(); 49 | menuGroupRepository = new InMemoryMenuGroupRepository(); 50 | productRepository = new InMemoryProductRepository(); 51 | purgomalumClient = new FakePurgomalumClient(); 52 | menuService = new MenuService(menuRepository, menuGroupRepository, productRepository, purgomalumClient); 53 | menuGroupId = menuGroupRepository.save(menuGroup()).getId(); 54 | product = productRepository.save(product("후라이드", 16_000L)); 55 | } 56 | 57 | @DisplayName("1개 이상의 등록된 상품으로 메뉴를 등록할 수 있다.") 58 | @Test 59 | void create() { 60 | final Menu expected = createMenuRequest( 61 | "후라이드+후라이드", 19_000L, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) 62 | ); 63 | final Menu actual = menuService.create(expected); 64 | assertThat(actual).isNotNull(); 65 | assertAll( 66 | () -> assertThat(actual.getId()).isNotNull(), 67 | () -> assertThat(actual.getName()).isEqualTo(expected.getName()), 68 | () -> assertThat(actual.getPrice()).isEqualTo(expected.getPrice()), 69 | () -> assertThat(actual.getMenuGroup().getId()).isEqualTo(expected.getMenuGroupId()), 70 | () -> assertThat(actual.isDisplayed()).isEqualTo(expected.isDisplayed()), 71 | () -> assertThat(actual.getMenuProducts()).hasSize(1) 72 | ); 73 | } 74 | 75 | @DisplayName("상품이 없으면 등록할 수 없다.") 76 | @MethodSource("menuProducts") 77 | @ParameterizedTest 78 | void create(final List menuProducts) { 79 | final Menu expected = createMenuRequest("후라이드+후라이드", 19_000L, menuGroupId, true, menuProducts); 80 | assertThatThrownBy(() -> menuService.create(expected)) 81 | .isInstanceOf(IllegalArgumentException.class); 82 | } 83 | 84 | private static List menuProducts() { 85 | return Arrays.asList( 86 | null, 87 | Arguments.of(Collections.emptyList()), 88 | Arguments.of(Arrays.asList(createMenuProductRequest(INVALID_ID, 2L))) 89 | ); 90 | } 91 | 92 | @DisplayName("메뉴에 속한 상품의 수량은 0개 이상이어야 한다.") 93 | @Test 94 | void createNegativeQuantity() { 95 | final Menu expected = createMenuRequest( 96 | "후라이드+후라이드", 19_000L, menuGroupId, true, createMenuProductRequest(product.getId(), -1L) 97 | ); 98 | assertThatThrownBy(() -> menuService.create(expected)) 99 | .isInstanceOf(IllegalArgumentException.class); 100 | } 101 | 102 | @DisplayName("메뉴의 가격이 올바르지 않으면 등록할 수 없다.") 103 | @ValueSource(strings = "-1000") 104 | @NullSource 105 | @ParameterizedTest 106 | void create(final BigDecimal price) { 107 | final Menu expected = createMenuRequest( 108 | "후라이드+후라이드", price, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) 109 | ); 110 | assertThatThrownBy(() -> menuService.create(expected)) 111 | .isInstanceOf(IllegalArgumentException.class); 112 | } 113 | 114 | @DisplayName("메뉴에 속한 상품 금액의 합은 메뉴의 가격보다 크거나 같아야 한다.") 115 | @Test 116 | void createExpensiveMenu() { 117 | final Menu expected = createMenuRequest( 118 | "후라이드+후라이드", 33_000L, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) 119 | ); 120 | assertThatThrownBy(() -> menuService.create(expected)) 121 | .isInstanceOf(IllegalArgumentException.class); 122 | } 123 | 124 | @DisplayName("메뉴는 특정 메뉴 그룹에 속해야 한다.") 125 | @NullSource 126 | @ParameterizedTest 127 | void create(final UUID menuGroupId) { 128 | final Menu expected = createMenuRequest( 129 | "후라이드+후라이드", 19_000L, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) 130 | ); 131 | assertThatThrownBy(() -> menuService.create(expected)) 132 | .isInstanceOf(NoSuchElementException.class); 133 | } 134 | 135 | @DisplayName("메뉴의 이름이 올바르지 않으면 등록할 수 없다.") 136 | @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) 137 | @NullSource 138 | @ParameterizedTest 139 | void create(final String name) { 140 | final Menu expected = createMenuRequest( 141 | name, 19_000L, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) 142 | ); 143 | assertThatThrownBy(() -> menuService.create(expected)) 144 | .isInstanceOf(IllegalArgumentException.class); 145 | } 146 | 147 | @DisplayName("메뉴의 가격을 변경할 수 있다.") 148 | @Test 149 | void changePrice() { 150 | final UUID menuId = menuRepository.save(menu(19_000L, menuProduct(product, 2L))).getId(); 151 | final Menu expected = changePriceRequest(16_000L); 152 | final Menu actual = menuService.changePrice(menuId, expected); 153 | assertThat(actual.getPrice()).isEqualTo(expected.getPrice()); 154 | } 155 | 156 | @DisplayName("메뉴의 가격이 올바르지 않으면 변경할 수 없다.") 157 | @ValueSource(strings = "-1000") 158 | @NullSource 159 | @ParameterizedTest 160 | void changePrice(final BigDecimal price) { 161 | final UUID menuId = menuRepository.save(menu(19_000L, menuProduct(product, 2L))).getId(); 162 | final Menu expected = changePriceRequest(price); 163 | assertThatThrownBy(() -> menuService.changePrice(menuId, expected)) 164 | .isInstanceOf(IllegalArgumentException.class); 165 | } 166 | 167 | @DisplayName("메뉴에 속한 상품 금액의 합은 메뉴의 가격보다 크거나 같아야 한다.") 168 | @Test 169 | void changePriceToExpensive() { 170 | final UUID menuId = menuRepository.save(menu(19_000L, menuProduct(product, 2L))).getId(); 171 | final Menu expected = changePriceRequest(33_000L); 172 | assertThatThrownBy(() -> menuService.changePrice(menuId, expected)) 173 | .isInstanceOf(IllegalArgumentException.class); 174 | } 175 | 176 | @DisplayName("메뉴를 노출할 수 있다.") 177 | @Test 178 | void display() { 179 | final UUID menuId = menuRepository.save(menu(19_000L, false, menuProduct(product, 2L))).getId(); 180 | final Menu actual = menuService.display(menuId); 181 | assertThat(actual.isDisplayed()).isTrue(); 182 | } 183 | 184 | @DisplayName("메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 높을 경우 메뉴를 노출할 수 없다.") 185 | @Test 186 | void displayExpensiveMenu() { 187 | final UUID menuId = menuRepository.save(menu(33_000L, false, menuProduct(product, 2L))).getId(); 188 | assertThatThrownBy(() -> menuService.display(menuId)) 189 | .isInstanceOf(IllegalStateException.class); 190 | } 191 | 192 | @DisplayName("메뉴를 숨길 수 있다.") 193 | @Test 194 | void hide() { 195 | final UUID menuId = menuRepository.save(menu(19_000L, true, menuProduct(product, 2L))).getId(); 196 | final Menu actual = menuService.hide(menuId); 197 | assertThat(actual.isDisplayed()).isFalse(); 198 | } 199 | 200 | @DisplayName("메뉴의 목록을 조회할 수 있다.") 201 | @Test 202 | void findAll() { 203 | menuRepository.save(menu(19_000L, true, menuProduct(product, 2L))); 204 | final List actual = menuService.findAll(); 205 | assertThat(actual).hasSize(1); 206 | } 207 | 208 | private Menu createMenuRequest( 209 | final String name, 210 | final long price, 211 | final UUID menuGroupId, 212 | final boolean displayed, 213 | final MenuProduct... menuProducts 214 | ) { 215 | return createMenuRequest(name, BigDecimal.valueOf(price), menuGroupId, displayed, menuProducts); 216 | } 217 | 218 | private Menu createMenuRequest( 219 | final String name, 220 | final BigDecimal price, 221 | final UUID menuGroupId, 222 | final boolean displayed, 223 | final MenuProduct... menuProducts 224 | ) { 225 | return createMenuRequest(name, price, menuGroupId, displayed, Arrays.asList(menuProducts)); 226 | } 227 | 228 | private Menu createMenuRequest( 229 | final String name, 230 | final long price, 231 | final UUID menuGroupId, 232 | final boolean displayed, 233 | final List menuProducts 234 | ) { 235 | return createMenuRequest(name, BigDecimal.valueOf(price), menuGroupId, displayed, menuProducts); 236 | } 237 | 238 | private Menu createMenuRequest( 239 | final String name, 240 | final BigDecimal price, 241 | final UUID menuGroupId, 242 | final boolean displayed, 243 | final List menuProducts 244 | ) { 245 | final Menu menu = new Menu(); 246 | menu.setName(name); 247 | menu.setPrice(price); 248 | menu.setMenuGroupId(menuGroupId); 249 | menu.setDisplayed(displayed); 250 | menu.setMenuProducts(menuProducts); 251 | return menu; 252 | } 253 | 254 | private static MenuProduct createMenuProductRequest(final UUID productId, final long quantity) { 255 | final MenuProduct menuProduct = new MenuProduct(); 256 | menuProduct.setProductId(productId); 257 | menuProduct.setQuantity(quantity); 258 | return menuProduct; 259 | } 260 | 261 | private Menu changePriceRequest(final long price) { 262 | return changePriceRequest(BigDecimal.valueOf(price)); 263 | } 264 | 265 | private Menu changePriceRequest(final BigDecimal price) { 266 | final Menu menu = new Menu(); 267 | menu.setPrice(price); 268 | return menu; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/products/application/FakePurgomalumClient.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.application; 2 | 3 | import kitchenpos.products.infra.PurgomalumClient; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class FakePurgomalumClient implements PurgomalumClient { 9 | private static final List profanities; 10 | 11 | static { 12 | profanities = Arrays.asList("비속어", "욕설"); 13 | } 14 | 15 | @Override 16 | public boolean containsProfanity(final String text) { 17 | return profanities.stream() 18 | .anyMatch(profanity -> text.contains(profanity)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/products/application/InMemoryProductRepository.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.application; 2 | 3 | import kitchenpos.products.domain.Product; 4 | import kitchenpos.products.domain.ProductRepository; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class InMemoryProductRepository implements ProductRepository { 14 | private final Map products = new HashMap<>(); 15 | 16 | @Override 17 | public Product save(final Product product) { 18 | products.put(product.getId(), product); 19 | return product; 20 | } 21 | 22 | @Override 23 | public Optional findById(final UUID id) { 24 | return Optional.ofNullable(products.get(id)); 25 | } 26 | 27 | @Override 28 | public List findAll() { 29 | return new ArrayList<>(products.values()); 30 | } 31 | 32 | @Override 33 | public List findAllByIdIn(final List ids) { 34 | return products.values() 35 | .stream() 36 | .filter(product -> ids.contains(product.getId())) 37 | .toList(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/kitchenpos/products/application/ProductServiceTest.java: -------------------------------------------------------------------------------- 1 | package kitchenpos.products.application; 2 | 3 | import kitchenpos.menus.application.InMemoryMenuRepository; 4 | import kitchenpos.menus.domain.Menu; 5 | import kitchenpos.menus.domain.MenuRepository; 6 | import kitchenpos.products.domain.Product; 7 | import kitchenpos.products.domain.ProductRepository; 8 | import kitchenpos.products.infra.PurgomalumClient; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.NullSource; 14 | import org.junit.jupiter.params.provider.ValueSource; 15 | 16 | import java.math.BigDecimal; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | import static kitchenpos.Fixtures.menu; 21 | import static kitchenpos.Fixtures.menuProduct; 22 | import static kitchenpos.Fixtures.product; 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 25 | import static org.junit.jupiter.api.Assertions.assertAll; 26 | 27 | class ProductServiceTest { 28 | private ProductRepository productRepository; 29 | private MenuRepository menuRepository; 30 | private PurgomalumClient purgomalumClient; 31 | private ProductService productService; 32 | 33 | @BeforeEach 34 | void setUp() { 35 | productRepository = new InMemoryProductRepository(); 36 | menuRepository = new InMemoryMenuRepository(); 37 | purgomalumClient = new FakePurgomalumClient(); 38 | productService = new ProductService(productRepository, menuRepository, purgomalumClient); 39 | } 40 | 41 | @DisplayName("상품을 등록할 수 있다.") 42 | @Test 43 | void create() { 44 | final Product expected = createProductRequest("후라이드", 16_000L); 45 | final Product actual = productService.create(expected); 46 | assertThat(actual).isNotNull(); 47 | assertAll( 48 | () -> assertThat(actual.getId()).isNotNull(), 49 | () -> assertThat(actual.getName()).isEqualTo(expected.getName()), 50 | () -> assertThat(actual.getPrice()).isEqualTo(expected.getPrice()) 51 | ); 52 | } 53 | 54 | @DisplayName("상품의 가격이 올바르지 않으면 등록할 수 없다.") 55 | @ValueSource(strings = "-1000") 56 | @NullSource 57 | @ParameterizedTest 58 | void create(final BigDecimal price) { 59 | final Product expected = createProductRequest("후라이드", price); 60 | assertThatThrownBy(() -> productService.create(expected)) 61 | .isInstanceOf(IllegalArgumentException.class); 62 | } 63 | 64 | @DisplayName("상품의 이름이 올바르지 않으면 등록할 수 없다.") 65 | @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) 66 | @NullSource 67 | @ParameterizedTest 68 | void create(final String name) { 69 | final Product expected = createProductRequest(name, 16_000L); 70 | assertThatThrownBy(() -> productService.create(expected)) 71 | .isInstanceOf(IllegalArgumentException.class); 72 | } 73 | 74 | @DisplayName("상품의 가격을 변경할 수 있다.") 75 | @Test 76 | void changePrice() { 77 | final UUID productId = productRepository.save(product("후라이드", 16_000L)).getId(); 78 | final Product expected = changePriceRequest(15_000L); 79 | final Product actual = productService.changePrice(productId, expected); 80 | assertThat(actual.getPrice()).isEqualTo(expected.getPrice()); 81 | } 82 | 83 | @DisplayName("상품의 가격이 올바르지 않으면 변경할 수 없다.") 84 | @ValueSource(strings = "-1000") 85 | @NullSource 86 | @ParameterizedTest 87 | void changePrice(final BigDecimal price) { 88 | final UUID productId = productRepository.save(product("후라이드", 16_000L)).getId(); 89 | final Product expected = changePriceRequest(price); 90 | assertThatThrownBy(() -> productService.changePrice(productId, expected)) 91 | .isInstanceOf(IllegalArgumentException.class); 92 | } 93 | 94 | @DisplayName("상품의 가격이 변경될 때 메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 크면 메뉴가 숨겨진다.") 95 | @Test 96 | void changePriceInMenu() { 97 | final Product product = productRepository.save(product("후라이드", 16_000L)); 98 | final Menu menu = menuRepository.save(menu(19_000L, true, menuProduct(product, 2L))); 99 | productService.changePrice(product.getId(), changePriceRequest(8_000L)); 100 | assertThat(menuRepository.findById(menu.getId()).get().isDisplayed()).isFalse(); 101 | } 102 | 103 | @DisplayName("상품의 목록을 조회할 수 있다.") 104 | @Test 105 | void findAll() { 106 | productRepository.save(product("후라이드", 16_000L)); 107 | productRepository.save(product("양념치킨", 16_000L)); 108 | final List actual = productService.findAll(); 109 | assertThat(actual).hasSize(2); 110 | } 111 | 112 | private Product createProductRequest(final String name, final long price) { 113 | return createProductRequest(name, BigDecimal.valueOf(price)); 114 | } 115 | 116 | private Product createProductRequest(final String name, final BigDecimal price) { 117 | final Product product = new Product(); 118 | product.setName(name); 119 | product.setPrice(price); 120 | return product; 121 | } 122 | 123 | private Product changePriceRequest(final long price) { 124 | return changePriceRequest(BigDecimal.valueOf(price)); 125 | } 126 | 127 | private Product changePriceRequest(final BigDecimal price) { 128 | final Product product = new Product(); 129 | product.setPrice(price); 130 | return product; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 2 | spring.datasource.username=sa 3 | spring.flyway.enabled=false 4 | spring.jpa.properties.hibernate.format_sql=true 5 | spring.jpa.show-sql=true 6 | logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE 7 | --------------------------------------------------------------------------------