From d197bc997bbd2fac4d01fb749e0c76af9a9f4bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:11:58 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=EC=A7=81=EC=A0=91=20=EA=B3=84?= =?UTF-8?q?=EC=A2=8C=EC=9D=B4=EC=B2=B4=20=EC=A3=BC=EB=AC=B8=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EA=B2=B0=EC=A0=9C=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B3=84=EC=A2=8C=20=EC=A0=95=EB=B3=B4=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/order/order/model/Order.java | 3 +- .../domain/payment/model/entity/Payment.java | 53 ++++++++++++++++--- .../koin/domain/shop/model/shop/Shop.java | 6 +++ ...1__support_direct_bank_transfer_orders.sql | 13 +++++ 4 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql diff --git a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java index e824cc6b43..76917b2aa9 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java @@ -48,9 +48,8 @@ public class Order extends BaseEntity { @Column(name = "id", nullable = false, updatable = false) Integer id; - @NotBlank @Size(min = 6, max = 64) - @Column(name = "pg_order_id", length = 64, nullable = false, updatable = false) + @Column(name = "pg_order_id", length = 64, updatable = false) private String pgOrderId; @NotBlank diff --git a/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java b/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java index 10c5a6ad5c..907d0cc2de 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java +++ b/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java @@ -12,6 +12,7 @@ import in.koreatech.koin.common.model.BaseEntity; import in.koreatech.koin.domain.order.order.model.Order; +import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -21,7 +22,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Builder; @@ -39,9 +39,8 @@ public class Payment extends BaseEntity { @GeneratedValue(strategy = IDENTITY) private Integer id; - @NotBlank @Size(max = 200) - @Column(name = "payment_key", length = 200, nullable = false, updatable = false) + @Column(name = "payment_key", length = 200, updatable = false) private String paymentKey; @NotNull @@ -69,14 +68,24 @@ public class Payment extends BaseEntity { @Column(name = "requested_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMP") private LocalDateTime requestedAt; - @NotNull - @Column(name = "approved_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMP") + @Column(name = "approved_at", updatable = false, columnDefinition = "TIMESTAMP") private LocalDateTime approvedAt; - @NotNull - @Column(name = "receipt", nullable = false, updatable = false, columnDefinition = "TEXT") + @Column(name = "receipt", updatable = false, columnDefinition = "TEXT") private String receipt; + @Size(max = 10) + @Column(name = "bank", length = 10, updatable = false) + private String bank; + + @Size(max = 20) + @Column(name = "account_number", length = 20, updatable = false) + private String accountNumber; + + @Size(max = 100) + @Column(name = "account_holder", length = 100, updatable = false) + private String accountHolder; + @NotNull @Column(name = "is_deleted", nullable = false) private Boolean isDeleted = FALSE; @@ -96,6 +105,9 @@ public Payment( LocalDateTime requestedAt, LocalDateTime approvedAt, String receipt, + String bank, + String accountNumber, + String accountHolder, Boolean isDeleted, Order order ) { @@ -108,10 +120,37 @@ public Payment( this.requestedAt = requestedAt; this.approvedAt = approvedAt; this.receipt = receipt; + this.bank = bank; + this.accountNumber = accountNumber; + this.accountHolder = accountHolder; this.isDeleted = isDeleted; this.order = order; } + public static Payment createBankTransfer(Order order) { + Shop shop = order.getOrderableShop().getShop(); + String description = order.getOrderMenus().get(0).getMenuName(); + if (order.getOrderMenus().size() > 1) { + description += " 외 " + (order.getOrderMenus().size() - 1) + "건"; + } + return Payment.builder() + .order(order) + .amount(order.getTotalPrice()) + .paymentStatus(PaymentStatus.WAITING_FOR_DEPOSIT) + .paymentMethod(PaymentMethod.ACCOUNT_TRANSFER) + .description(description) + .requestedAt(LocalDateTime.now()) + .bank(shop.getBank()) + .accountNumber(shop.getAccountNumber()) + .accountHolder(shop.getAccountHolder()) + .isDeleted(false) + .build(); + } + + public boolean isDirectBankTransfer() { + return paymentMethod == PaymentMethod.ACCOUNT_TRANSFER && paymentKey == null; + } + public void validateUserIdMatches(Integer userId) { if (!userId.equals(this.order.getUser().getId())) { throw CustomException.of(PAYMENT_ACCESS_DENIED); diff --git a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java index 3b22431ce3..67ed6dfec0 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java +++ b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java @@ -159,6 +159,10 @@ public class Shop extends BaseEntity { @Column(name = "account_number", length = 20) private String accountNumber; + @Size(max = 100) + @Column(name = "account_holder", length = 100) + private String accountHolder; + @Column(name = "introduction", columnDefinition = "text") private String introduction; @@ -184,6 +188,7 @@ private Shop( Integer hit, String bank, String accountNumber, + String accountHolder, ShopCategory shopMainCategory, ShopOperation shopOperation ) { @@ -204,6 +209,7 @@ private Shop( this.hit = hit; this.bank = bank; this.accountNumber = accountNumber; + this.accountHolder = accountHolder; this.shopMainCategory = shopMainCategory; this.shopOperation = shopOperation; } diff --git a/src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql b/src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql new file mode 100644 index 0000000000..c9734167a3 --- /dev/null +++ b/src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql @@ -0,0 +1,13 @@ +ALTER TABLE `shops` + ADD COLUMN `account_holder` VARCHAR(100) NULL COMMENT '예금주'; + +ALTER TABLE `order_v2` + MODIFY COLUMN `pg_order_id` VARCHAR(64) NULL COMMENT 'PG 주문 ID. 직접 계좌이체 주문은 NULL'; + +ALTER TABLE `payment_v2` + MODIFY COLUMN `payment_key` VARCHAR(200) NULL COMMENT 'PG 결제 키', + MODIFY COLUMN `approved_at` TIMESTAMP NULL DEFAULT NULL COMMENT '결제 승인 일시', + MODIFY COLUMN `receipt` TEXT NULL COMMENT 'PG 영수증', + ADD COLUMN `bank` VARCHAR(10) NULL COMMENT '주문 당시 은행명', + ADD COLUMN `account_number` VARCHAR(20) NULL COMMENT '주문 당시 계좌번호', + ADD COLUMN `account_holder` VARCHAR(100) NULL COMMENT '주문 당시 예금주'; From 854acad0539ad28bdbdfb64745fcad6dbb340c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:14:20 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20PG=20=EC=8A=B9=EC=9D=B8=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EA=B3=84=EC=A2=8C=EC=9D=B4=EC=B2=B4=20?= =?UTF-8?q?=EB=B0=B0=EB=8B=AC=20=EC=A3=BC=EB=AC=B8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/cart/repository/CartRepository.java | 11 ++ .../order/order/controller/OrderApi.java | 22 ++++ .../order/controller/OrderController.java | 15 +++ .../dto/request/DeliveryOrderRequest.java | 74 +++++++++++ .../dto/response/OrderCreateResponse.java | 46 +++++++ .../order/service/DeliveryOrderService.java | 123 ++++++++++++++++++ .../domain/payment/controller/PaymentApi.java | 3 +- .../dto/response/BankAccountResponse.java | 28 ++++ .../payment/dto/response/PaymentResponse.java | 10 +- .../koin/global/code/ApiResponseCode.java | 1 + 10 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/order/order/dto/request/DeliveryOrderRequest.java create mode 100644 src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java create mode 100644 src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java create mode 100644 src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java diff --git a/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java b/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java index eb1bf96d7e..9ee160994e 100644 --- a/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java +++ b/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java @@ -3,12 +3,14 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import in.koreatech.koin.domain.order.cart.model.Cart; import in.koreatech.koin.global.code.ApiResponseCode; import in.koreatech.koin.global.exception.CustomException; +import jakarta.persistence.LockModeType; public interface CartRepository extends JpaRepository { @@ -26,5 +28,14 @@ default Cart getCartByUserId(Integer userId) { .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CART)); } + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT c FROM Cart c WHERE c.user.id = :userId") + Optional findCartByUserIdForUpdate(@Param("userId") Integer userId); + + default Cart getCartByUserIdForUpdate(Integer userId) { + return findCartByUserIdForUpdate(userId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CART)); + } + void deleteByUserId(Integer userId); } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java index fb3abf3451..5ff276699c 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java @@ -6,22 +6,44 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest; import in.koreatech.koin.domain.order.order.dto.request.OrderSearchPeriodCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderStatusCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderTypeCriteria; import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse; +import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; import in.koreatech.koin.global.auth.Auth; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; @Tag(name = "(Normal) Order: 주문", description = "주문을 관리한다.") @RequestMapping("/order") public interface OrderApi { + @Operation( + summary = "계좌이체 배달 주문을 생성한다.", + description = """ + 기존 배달 임시 결제 저장 API와 동일한 요청 바디를 사용한다. + PG 승인 및 임시 저장 없이 장바구니로 주문을 생성하고 장바구니를 비운다. + 계좌이체만 지원하며 앱에서 입금 여부를 확인하지 않는다. + 응답의 계좌와 amount로 입금 안내를 표시한다. + 이후 GET /payments/{paymentId}로 주문 및 계좌 정보를 재조회한다. + 중복 호출 또는 실패 후 재시도 전에는 주문 내역을 확인한다. + """ + ) + @PostMapping("/delivery") + ResponseEntity createDeliveryOrder( + @RequestBody @Valid DeliveryOrderRequest request, + @Auth(permit = {STUDENT}) Integer userId + ); + @Operation( summary = "주문 이력을 조회한다.", description = """ diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java index 211946b218..cddfac0e23 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java @@ -6,18 +6,24 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest; import in.koreatech.koin.domain.order.order.dto.request.OrderSearchCondition; import in.koreatech.koin.domain.order.order.dto.request.OrderSearchPeriodCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderStatusCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderTypeCriteria; import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse; +import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; +import in.koreatech.koin.domain.order.order.service.DeliveryOrderService; import in.koreatech.koin.domain.order.order.service.OrderService; import in.koreatech.koin.global.auth.Auth; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RestController @@ -26,6 +32,15 @@ public class OrderController implements OrderApi { private final OrderService orderService; + private final DeliveryOrderService deliveryOrderService; + + @PostMapping("/delivery") + public ResponseEntity createDeliveryOrder( + @RequestBody @Valid DeliveryOrderRequest request, + @Auth(permit = {STUDENT}) Integer userId + ) { + return ResponseEntity.ok(deliveryOrderService.createDeliveryOrder(userId, request)); + } @GetMapping public ResponseEntity getOrders( diff --git a/src/main/java/in/koreatech/koin/domain/order/order/dto/request/DeliveryOrderRequest.java b/src/main/java/in/koreatech/koin/domain/order/order/dto/request/DeliveryOrderRequest.java new file mode 100644 index 0000000000..031098d781 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/order/order/dto/request/DeliveryOrderRequest.java @@ -0,0 +1,74 @@ +package in.koreatech.koin.domain.order.order.dto.request; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.order.delivery.model.AddressType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record DeliveryOrderRequest( + @Schema(description = "배달 주소", example = "충청남도 천안시 동남구 병천면 충절로 1600", requiredMode = REQUIRED) + @NotBlank(message = "배달 주소는 필수 입력사항입니다.") + @Size(max = 100, message = "배달 주소는 100자 이하여야 합니다.") + String address, + + @Schema(description = "배달 상세 주소", example = "은솔관 422호", requiredMode = NOT_REQUIRED) + String addressDetail, + + @Schema(description = "배달 주소 경도", example = "127.28372942", requiredMode = REQUIRED) + @NotNull(message = "배달 주소 경도는 필수 입력사항입니다.") + BigDecimal longitude, + + @Schema(description = "배달 주소 위도", example = "36.76125794", requiredMode = REQUIRED) + @NotNull(message = "배달 주소 위도는 필수 입력사항입니다.") + BigDecimal latitude, + + @Schema(description = "연락처", example = "01012345678", requiredMode = REQUIRED) + @NotBlank(message = "연락처는 필수 입력사항입니다.") + @Pattern(regexp = "^\\d{11}$", message = "전화번호 형식이 올바르지 않습니다. 11자리 숫자로 입력해 주세요.") + String phoneNumber, + + @Schema(description = "사장님에게", example = "리뷰 이벤트 감사합니다.", requiredMode = NOT_REQUIRED) + @Size(max = 50, message = "사장님 요청 사항은 50자 이하여야 합니다.") + String toOwner, + + @Schema(description = "라이더에게", example = "문 앞에 놔주세요.", requiredMode = NOT_REQUIRED) + @Size(max = 50, message = "라이더 요청 사항은 50자 이하여야 합니다.") + String toRider, + + @Schema(description = "메뉴 총 금액", example = "1234", requiredMode = REQUIRED) + @NotNull(message = "메뉴 총 금액은 필수 입력사항입니다.") + @PositiveOrZero(message = "금액은 0 이상이어야 합니다.") + Integer totalMenuPrice, + + @Schema(description = "배달 타입", example = "OFF_CAMPUS", requiredMode = REQUIRED) + @NotNull(message = "배달 타입은 필수 입력사항입니다.") + AddressType deliveryType, + + @Schema(description = "배달 팁", example = "1234", requiredMode = REQUIRED) + @NotNull(message = "배달 팁은 필수 입력사항입니다.") + @PositiveOrZero(message = "금액은 0 이상이어야 합니다.") + Integer deliveryTip, + + @Schema(description = "수저, 포크 수령 여부", example = "true", requiredMode = REQUIRED) + @NotNull(message = "수저, 포크 수령 여부는 필수 입력사항입니다.") + Boolean provideCutlery, + + @Schema(description = "결제 금액", example = "10000", requiredMode = REQUIRED) + @NotNull(message = "결제 금액은 필수 입력사항입니다.") + @PositiveOrZero(message = "금액은 0 이상이어야 합니다.") + Integer totalAmount +) { + +} diff --git a/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java b/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java new file mode 100644 index 0000000000..26a95896cc --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java @@ -0,0 +1,46 @@ +package in.koreatech.koin.domain.order.order.dto.response; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.order.order.model.Order; +import in.koreatech.koin.domain.payment.dto.response.BankAccountResponse; +import in.koreatech.koin.domain.payment.model.entity.Payment; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record OrderCreateResponse( + @Schema(description = "주문 ID", example = "123", requiredMode = REQUIRED) + Integer orderId, + + @Schema(description = "주문 번호", example = "A1B2C3D4E5", requiredMode = REQUIRED) + String orderNumber, + + @Schema(description = "결제 ID. GET /payments/{paymentId} 재조회에 사용한다.", example = "456", requiredMode = REQUIRED) + Integer paymentId, + + @Schema(description = "주문 유형", example = "DELIVERY", requiredMode = REQUIRED) + String orderType, + + @Schema(description = "주문 상태", example = "CONFIRMING", requiredMode = REQUIRED) + String orderStatus, + + @Schema(description = "결제 방법", example = "계좌이체", requiredMode = REQUIRED) + String paymentMethod, + + @Schema(description = "입금할 금액. 배달비를 포함한다.", example = "18000", requiredMode = REQUIRED) + Integer amount, + + @Schema(description = "주문 당시 상점 계좌 정보", requiredMode = REQUIRED) + BankAccountResponse bankAccount +) { + public static OrderCreateResponse of(Order order, Payment payment) { + return new OrderCreateResponse( + order.getId(), order.getOrderNumber(), payment.getId(), order.getOrderType().name(), + order.getStatus().name(), payment.getPaymentMethod().getDisplayName(), payment.getAmount(), + BankAccountResponse.from(payment) + ); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java new file mode 100644 index 0000000000..d1895b2858 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -0,0 +1,123 @@ +package in.koreatech.koin.domain.order.order.service; + +import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; +import static in.koreatech.koin.domain.order.order.model.OrderStatus.CONFIRMING; +import static in.koreatech.koin.domain.order.order.model.OrderType.DELIVERY; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import in.koreatech.koin.domain.address.service.AddressValidator; +import in.koreatech.koin.domain.order.cart.model.Cart; +import in.koreatech.koin.domain.order.cart.repository.CartRepository; +import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest; +import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; +import in.koreatech.koin.domain.order.order.model.Order; +import in.koreatech.koin.domain.order.order.model.OrderDelivery; +import in.koreatech.koin.domain.order.order.repository.OrderRepository; +import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; +import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; +import in.koreatech.koin.domain.payment.model.domain.DeliveryPaymentInfo; +import in.koreatech.koin.domain.payment.model.entity.Payment; +import in.koreatech.koin.domain.payment.repository.PaymentRepository; +import in.koreatech.koin.domain.shop.model.shop.Shop; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.global.code.ApiResponseCode; +import in.koreatech.koin.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class DeliveryOrderService { + + private final UserRepository userRepository; + private final CartRepository cartRepository; + private final OrderRepository orderRepository; + private final PaymentRepository paymentRepository; + private final OrderMenuItemsMapper orderMenuItemsMapper; + private final OrderNumberGenerator orderNumberGenerator; + private final AddressValidator addressValidator; + + @Transactional + public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequest request) { + User user = userRepository.getById(userId); + if (request.deliveryType() == OFF_CAMPUS) { + addressValidator.validateAddress(request.address()); + } + + Cart cart = cartRepository.getCartByUserIdForUpdate(userId); + validateCart(cart); + OrderableShop orderableShop = cart.getOrderableShop(); + validateBankAccount(orderableShop.getShop()); + + int totalProductPrice = cart.calculateItemsAmount(); + int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); + int totalPrice = totalProductPrice + deliveryFee; + DeliveryPaymentInfo.of( + request.phoneNumber(), request.address(), request.addressDetail(), request.longitude(), request.latitude(), + request.toOwner(), request.toRider(), request.provideCutlery(), request.totalMenuPrice(), + request.deliveryTip(), request.totalAmount() + ).validatePrice(totalProductPrice, deliveryFee, totalPrice); + + Order order = createOrder(user, orderableShop, request, totalProductPrice, deliveryFee, totalPrice); + orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); + orderRepository.save(order); + + Payment payment = Payment.createBankTransfer(order); + paymentRepository.save(payment); + cartRepository.delete(cart); + + return OrderCreateResponse.of(order, payment); + } + + private void validateCart(Cart cart) { + if (cart.getCartMenuItems().isEmpty()) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_CART_ITEM); + } + cart.validateOrderType(DELIVERY); + cart.getOrderableShop().requireShopOpen(); + cart.getOrderableShop().requireMinimumOrderAmount(cart.calculateItemsAmount()); + cart.getCartMenuItems().forEach(item -> item.getOrderableShopMenu().validateSoldOut()); + } + + private void validateBankAccount(Shop shop) { + if (!shop.isPayBank() || !StringUtils.hasText(shop.getBank()) + || !StringUtils.hasText(shop.getAccountNumber()) || !StringUtils.hasText(shop.getAccountHolder())) { + throw CustomException.of(ApiResponseCode.SHOP_BANK_ACCOUNT_UNAVAILABLE); + } + } + + private Order createOrder(User user, OrderableShop orderableShop, DeliveryOrderRequest request, + int totalProductPrice, int deliveryFee, int totalPrice) { + Shop shop = orderableShop.getShop(); + Order order = Order.builder() + .orderNumber(orderNumberGenerator.generate()) + .orderType(DELIVERY) + .status(CONFIRMING) + .orderableShopAddress(shop.getAddress()) + .orderableShopAddressDetail(shop.getAddressDetail()) + .orderableShopName(shop.getName()) + .phoneNumber(request.phoneNumber()) + .totalProductPrice(totalProductPrice) + .discountAmount(0) + .totalPrice(totalPrice) + .orderableShop(orderableShop) + .user(user) + .isDeleted(false) + .build(); + order.setOrderDelivery(OrderDelivery.builder() + .order(order) + .address(request.address()) + .addressDetail(request.addressDetail()) + .longitude(request.longitude()) + .latitude(request.latitude()) + .toOwner(request.toOwner()) + .toRider(request.toRider()) + .provideCutlery(request.provideCutlery()) + .deliveryTip(deliveryFee) + .build()); + return order; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java b/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java index 4c67ca3d9a..afef522d28 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java +++ b/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java @@ -146,10 +146,11 @@ ResponseEntity cancelPayment( - `order_type`: 주문 방법 (DELIVERY | TAKE_OUT) - `easy_pay_company`: 간편결제사 - `requested_at`: 결제 요청 일시 (yyyy.MM.dd HH:mm) - - `approved_at`: 결제 승인 일시 (yyyy.MM.dd HH:mm) + - `approved_at`: 결제 승인 일시 (yyyy.MM.dd HH:mm). 직접 계좌이체 주문은 null - `payment_method`: 결제 수단 - `estimated_at`: 예상 시각 (HH:mm) - `order_status`: 주문 상태 + - `bank_account`: 직접 계좌이체 주문 당시 은행명(bank), 계좌번호(account_number), 예금주(account_holder) """ ) @GetMapping("/{paymentId}") diff --git a/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java b/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java new file mode 100644 index 0000000000..ebc06bcfed --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java @@ -0,0 +1,28 @@ +package in.koreatech.koin.domain.payment.dto.response; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.payment.model.entity.Payment; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record BankAccountResponse( + @Schema(description = "은행명", example = "국민은행", requiredMode = REQUIRED) + String bank, + + @Schema(description = "계좌번호", example = "123456-78-901234", requiredMode = REQUIRED) + String accountNumber, + + @Schema(description = "예금주", example = "홍길동", requiredMode = REQUIRED) + String accountHolder +) { + public static BankAccountResponse from(Payment payment) { + if (!payment.isDirectBankTransfer()) { + return null; + } + return new BankAccountResponse(payment.getBank(), payment.getAccountNumber(), payment.getAccountHolder()); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java b/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java index 90da0402cc..2fcce71441 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java +++ b/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java @@ -82,7 +82,7 @@ public record PaymentResponse( @JsonFormat(pattern = "yyyy.MM.dd HH:mm") LocalDateTime requestedAt, - @Schema(description = "결제 승인 일시", example = "2025.06.21 21:00", requiredMode = REQUIRED) + @Schema(description = "결제 승인 일시. 직접 계좌이체 주문은 null", example = "2025.06.21 21:00", requiredMode = NOT_REQUIRED) @JsonFormat(pattern = "yyyy.MM.dd HH:mm") LocalDateTime approvedAt, @@ -94,7 +94,10 @@ public record PaymentResponse( LocalTime estimatedAt, @Schema(description = "주문 상태", example = "COOKING", requiredMode = REQUIRED) - String orderStatus + String orderStatus, + + @Schema(description = "직접 계좌이체 주문 당시 상점 계좌 정보", requiredMode = NOT_REQUIRED) + BankAccountResponse bankAccount ) { @JsonNaming(value = SnakeCaseStrategy.class) @@ -208,7 +211,8 @@ public static PaymentResponse of( payment.getApprovedAt(), payment.getPaymentMethod().getDisplayName(), estimatedTime, - order.getStatus().name() + order.getStatus().name(), + BankAccountResponse.from(payment) ); } } diff --git a/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java b/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java index add108fd9f..46a3a2d52c 100644 --- a/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java +++ b/src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java @@ -78,6 +78,7 @@ public enum ApiResponseCode { ORDER_AMOUNT_BELOW_MINIMUM(HttpStatus.BAD_REQUEST, "최소 주문 금액을 충족하지 않습니다."), INVALID_SELF_CHAT(HttpStatus.BAD_REQUEST, "자신이 올린 게시글에 메시지를 보낼 수 없습니다."), INVALID_WEBSOCKET_USER_SESSION(HttpStatus.BAD_REQUEST, "웹소켓 사용자 세션 탐색 실패"), + SHOP_BANK_ACCOUNT_UNAVAILABLE(HttpStatus.BAD_REQUEST, "상점의 계좌이체 정보가 등록되지 않았습니다."), ORDER_PRICE_MISMATCH(HttpStatus.BAD_REQUEST, "클라이언트 요청 금액이 서버 계산 결과와 다릅니다."), MISMATCH_TEMPORARY_PAYMENT(HttpStatus.BAD_REQUEST, "요청한 정보가 임시 결제 정보와 일치하지 않습니다."), PAYMENT_ALREADY_CANCELED(HttpStatus.BAD_REQUEST, "이미 취소된 결제입니다."), From 0cad90274fce9b1438798290d3f10e72291f975d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:14:56 +0900 Subject: [PATCH 03/20] =?UTF-8?q?fix:=20=EC=A7=81=EC=A0=91=20=EA=B3=84?= =?UTF-8?q?=EC=A2=8C=EC=9D=B4=EC=B2=B4=20=EC=A3=BC=EB=AC=B8=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EC=8B=9C=20PG=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/service/PaymentCancelService.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java b/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java index 8ab747bb85..c49fa4335e 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java +++ b/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java @@ -3,11 +3,14 @@ import static in.koreatech.koin.global.code.ApiResponseCode.PAYMENT_ALREADY_CANCELED; import static in.koreatech.koin.global.code.ApiResponseCode.PAYMENT_CANCEL_ERROR; +import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import in.koreatech.koin.domain.order.order.model.OrderStatus; import in.koreatech.koin.domain.payment.dto.response.PaymentCancelResponse; import in.koreatech.koin.domain.payment.gateway.pg.PaymentGatewayService; import in.koreatech.koin.domain.payment.gateway.pg.dto.PaymentGatewayCancelResponse; @@ -50,6 +53,9 @@ public PaymentCancelResponse cancelPaymentByOrderId(Integer orderId, String canc } private List cancel(Payment payment, String cancelReason) { + if (payment.isDirectBankTransfer()) { + return cancelBankTransferOrder(payment, cancelReason); + } String paymentIdempotencyKey = paymentIdempotencyKeyService.getOrCreate(payment.getOrder().getUser().getId()); PaymentGatewayCancelResponse pgResponse = paymentGatewayService.cancelPayment(payment.getPaymentKey(), cancelReason, paymentIdempotencyKey); @@ -62,6 +68,23 @@ private List cancel(Payment payment, String cancelReason) { return paymentCancels; } + private List cancelBankTransferOrder(Payment payment, String cancelReason) { + payment.getOrder().requireStatusChangeableTo(OrderStatus.CANCELED); + payment.cancel(cancelReason); + PaymentCancel paymentCancel = PaymentCancel.builder() + .transactionKey(UUID.randomUUID().toString()) + .cancelReason(cancelReason) + // 입금과 환불은 앱에서 확인하지 않으므로 자동 환불 금액은 0원이다. + .cancelAmount(0) + .canceledAt(LocalDateTime.now()) + .isDeleted(false) + .payment(payment) + .build(); + List paymentCancels = List.of(paymentCancel); + paymentCancelRepository.saveAll(paymentCancels); + return paymentCancels; + } + private void validatePaymentStatusIsNotCanceled(Payment payment) { if (payment.getPaymentStatus().isCanceled()) { throw CustomException.of(PAYMENT_ALREADY_CANCELED); From c6344f2468f2d04af65720cda6b4a815c129e66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:16:10 +0900 Subject: [PATCH 04/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20=EC=83=9D=EC=84=B1=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/order/order/controller/OrderController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java index cddfac0e23..400a957a35 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java @@ -39,7 +39,8 @@ public ResponseEntity createDeliveryOrder( @RequestBody @Valid DeliveryOrderRequest request, @Auth(permit = {STUDENT}) Integer userId ) { - return ResponseEntity.ok(deliveryOrderService.createDeliveryOrder(userId, request)); + OrderCreateResponse response = deliveryOrderService.createDeliveryOrder(userId, request); + return ResponseEntity.ok(response); } @GetMapping From 152c789f5b291c060840f348d598371fad13218c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:17:01 +0900 Subject: [PATCH 05/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=B9=84=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=20=EC=A0=9C=EA=B1=B0=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/order/cart/repository/CartRepository.java | 11 ----------- .../order/order/service/DeliveryOrderService.java | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java b/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java index 9ee160994e..eb1bf96d7e 100644 --- a/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java +++ b/src/main/java/in/koreatech/koin/domain/order/cart/repository/CartRepository.java @@ -3,14 +3,12 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import in.koreatech.koin.domain.order.cart.model.Cart; import in.koreatech.koin.global.code.ApiResponseCode; import in.koreatech.koin.global.exception.CustomException; -import jakarta.persistence.LockModeType; public interface CartRepository extends JpaRepository { @@ -28,14 +26,5 @@ default Cart getCartByUserId(Integer userId) { .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CART)); } - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT c FROM Cart c WHERE c.user.id = :userId") - Optional findCartByUserIdForUpdate(@Param("userId") Integer userId); - - default Cart getCartByUserIdForUpdate(Integer userId) { - return findCartByUserIdForUpdate(userId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CART)); - } - void deleteByUserId(Integer userId); } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index d1895b2858..a1173130f4 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -47,7 +47,7 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ addressValidator.validateAddress(request.address()); } - Cart cart = cartRepository.getCartByUserIdForUpdate(userId); + Cart cart = cartRepository.getCartByUserId(userId); validateCart(cart); OrderableShop orderableShop = cart.getOrderableShop(); validateBankAccount(orderableShop.getShop()); From 33715369eeeac280f9afb233fc4c57f5c5d4174c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:18:47 +0900 Subject: [PATCH 06/20] =?UTF-8?q?refactor:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=9D=91=EB=8B=B5=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/dto/response/OrderCreateResponse.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java b/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java index 26a95896cc..b82e0f4a18 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/dto/response/OrderCreateResponse.java @@ -18,7 +18,7 @@ public record OrderCreateResponse( @Schema(description = "주문 번호", example = "A1B2C3D4E5", requiredMode = REQUIRED) String orderNumber, - @Schema(description = "결제 ID. GET /payments/{paymentId} 재조회에 사용한다.", example = "456", requiredMode = REQUIRED) + @Schema(description = "결제 ID", example = "456", requiredMode = REQUIRED) Integer paymentId, @Schema(description = "주문 유형", example = "DELIVERY", requiredMode = REQUIRED) @@ -30,16 +30,21 @@ public record OrderCreateResponse( @Schema(description = "결제 방법", example = "계좌이체", requiredMode = REQUIRED) String paymentMethod, - @Schema(description = "입금할 금액. 배달비를 포함한다.", example = "18000", requiredMode = REQUIRED) + @Schema(description = "총 결제 금액", example = "18000", requiredMode = REQUIRED) Integer amount, - @Schema(description = "주문 당시 상점 계좌 정보", requiredMode = REQUIRED) + @Schema(description = "상점 계좌 정보", requiredMode = REQUIRED) BankAccountResponse bankAccount ) { public static OrderCreateResponse of(Order order, Payment payment) { return new OrderCreateResponse( - order.getId(), order.getOrderNumber(), payment.getId(), order.getOrderType().name(), - order.getStatus().name(), payment.getPaymentMethod().getDisplayName(), payment.getAmount(), + order.getId(), + order.getOrderNumber(), + payment.getId(), + order.getOrderType().name(), + order.getStatus().name(), + payment.getPaymentMethod().getDisplayName(), + payment.getAmount(), BankAccountResponse.from(payment) ); } From 5ac8a24ca98f9fe481b210c9b5b05013c96a982a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:20:46 +0900 Subject: [PATCH 07/20] =?UTF-8?q?fix:=20=EC=98=88=EA=B8=88=EC=A3=BC=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=A0=9C=EC=99=B8=20=EB=B0=8F=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=9E=84=EC=8B=9C=20=EC=B2=98=EB=A6=AC=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/order/order/service/DeliveryOrderService.java | 2 +- .../koin/domain/payment/dto/response/BankAccountResponse.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index a1173130f4..d5b5418d04 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -84,7 +84,7 @@ private void validateCart(Cart cart) { private void validateBankAccount(Shop shop) { if (!shop.isPayBank() || !StringUtils.hasText(shop.getBank()) - || !StringUtils.hasText(shop.getAccountNumber()) || !StringUtils.hasText(shop.getAccountHolder())) { + || !StringUtils.hasText(shop.getAccountNumber())) { throw CustomException.of(ApiResponseCode.SHOP_BANK_ACCOUNT_UNAVAILABLE); } } diff --git a/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java b/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java index ebc06bcfed..d99e6a2287 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java +++ b/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java @@ -23,6 +23,7 @@ public static BankAccountResponse from(Payment payment) { if (!payment.isDirectBankTransfer()) { return null; } - return new BankAccountResponse(payment.getBank(), payment.getAccountNumber(), payment.getAccountHolder()); + // TODO: 사장님 앱 작업 후 수정 + return new BankAccountResponse(payment.getBank(), payment.getAccountNumber(), ""); } } From 15335984f430618a409af286d7cb79602d85687c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:21:05 +0900 Subject: [PATCH 08/20] =?UTF-8?q?fix:=20=EC=98=88=EA=B8=88=EC=A3=BC=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EC=9D=91=EB=8B=B5=EA=B0=92=EC=9D=84=20?= =?UTF-8?q?=EC=9E=84=EA=B8=88=EC=A3=BC=EB=AA=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/payment/dto/response/BankAccountResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java b/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java index d99e6a2287..c39af3e045 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java +++ b/src/main/java/in/koreatech/koin/domain/payment/dto/response/BankAccountResponse.java @@ -24,6 +24,6 @@ public static BankAccountResponse from(Payment payment) { return null; } // TODO: 사장님 앱 작업 후 수정 - return new BankAccountResponse(payment.getBank(), payment.getAccountNumber(), ""); + return new BankAccountResponse(payment.getBank(), payment.getAccountNumber(), "임금주명"); } } From 5d45cfc1adcd9af21e78f456907186c9b1ae6044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:22:13 +0900 Subject: [PATCH 09/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/order/cart/model/Cart.java | 10 ++++++++++ .../order/order/service/DeliveryOrderService.java | 12 +----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java b/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java index 789ff4a6e3..8ee2f4b5bc 100644 --- a/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java +++ b/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java @@ -127,6 +127,16 @@ public void validateOrderType(OrderType orderType) { } } + public void validateDeliveryOrder() { + if (cartMenuItems.isEmpty()) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_CART_ITEM); + } + validateOrderType(OrderType.DELIVERY); + orderableShop.requireShopOpen(); + orderableShop.requireMinimumOrderAmount(calculateItemsAmount()); + cartMenuItems.forEach(item -> item.getOrderableShopMenu().validateSoldOut()); + } + public Integer getItemTypeCount() { return this.cartMenuItems.size(); } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index d5b5418d04..c04d0f403c 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -48,7 +48,7 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ } Cart cart = cartRepository.getCartByUserId(userId); - validateCart(cart); + cart.validateDeliveryOrder(); OrderableShop orderableShop = cart.getOrderableShop(); validateBankAccount(orderableShop.getShop()); @@ -72,16 +72,6 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ return OrderCreateResponse.of(order, payment); } - private void validateCart(Cart cart) { - if (cart.getCartMenuItems().isEmpty()) { - throw CustomException.of(ApiResponseCode.NOT_FOUND_CART_ITEM); - } - cart.validateOrderType(DELIVERY); - cart.getOrderableShop().requireShopOpen(); - cart.getOrderableShop().requireMinimumOrderAmount(cart.calculateItemsAmount()); - cart.getCartMenuItems().forEach(item -> item.getOrderableShopMenu().validateSoldOut()); - } - private void validateBankAccount(Shop shop) { if (!shop.isPayBank() || !StringUtils.hasText(shop.getBank()) || !StringUtils.hasText(shop.getAccountNumber())) { From c0584ebc84e5ad4051a8d4ebf0d9240a5fc91bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:23:20 +0900 Subject: [PATCH 10/20] =?UTF-8?q?refactor:=20=EA=B3=84=EC=A2=8C=EC=9D=B4?= =?UTF-8?q?=EC=B2=B4=20=EC=A0=95=EB=B3=B4=20=EA=B2=80=EC=A6=9D=EC=9D=84=20?= =?UTF-8?q?=EC=83=81=EC=A0=90=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/order/service/DeliveryOrderService.java | 12 +----------- .../koreatech/koin/domain/shop/model/shop/Shop.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index c04d0f403c..89eea47185 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -6,7 +6,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import in.koreatech.koin.domain.address.service.AddressValidator; import in.koreatech.koin.domain.order.cart.model.Cart; @@ -24,8 +23,6 @@ import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; -import in.koreatech.koin.global.code.ApiResponseCode; -import in.koreatech.koin.global.exception.CustomException; import lombok.RequiredArgsConstructor; @Service @@ -50,7 +47,7 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ Cart cart = cartRepository.getCartByUserId(userId); cart.validateDeliveryOrder(); OrderableShop orderableShop = cart.getOrderableShop(); - validateBankAccount(orderableShop.getShop()); + orderableShop.getShop().validateBankAccount(); int totalProductPrice = cart.calculateItemsAmount(); int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); @@ -72,13 +69,6 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ return OrderCreateResponse.of(order, payment); } - private void validateBankAccount(Shop shop) { - if (!shop.isPayBank() || !StringUtils.hasText(shop.getBank()) - || !StringUtils.hasText(shop.getAccountNumber())) { - throw CustomException.of(ApiResponseCode.SHOP_BANK_ACCOUNT_UNAVAILABLE); - } - } - private Order createOrder(User user, OrderableShop orderableShop, DeliveryOrderRequest request, int totalProductPrice, int deliveryFee, int totalPrice) { Shop shop = orderableShop.getShop(); diff --git a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java index 67ed6dfec0..a7a5b0983f 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java +++ b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java @@ -14,6 +14,7 @@ import java.util.Set; import org.hibernate.annotations.Where; +import org.springframework.util.StringUtils; import in.koreatech.koin.common.model.BaseEntity; import in.koreatech.koin.domain.order.shop.model.domain.ShopBaseDeliveryTips; @@ -24,6 +25,8 @@ import in.koreatech.koin.domain.shop.model.menu.Menu; import in.koreatech.koin.domain.shop.model.menu.MenuCategory; import in.koreatech.koin.domain.shop.model.review.ShopReview; +import in.koreatech.koin.global.code.ApiResponseCode; +import in.koreatech.koin.global.exception.CustomException; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -342,6 +345,13 @@ public String getFullAddress() { return String.join(" ", this.address, this.addressDetail); } + // TODO: 예금주명 추가되면 검증 로직 추가 필요 + public void validateBankAccount() { + if (!payBank || !StringUtils.hasText(bank) || !StringUtils.hasText(accountNumber)) { + throw CustomException.of(ApiResponseCode.SHOP_BANK_ACCOUNT_UNAVAILABLE); + } + } + public boolean isOwner(Integer ownerId) { return this.owner != null && this.owner.getId().equals(ownerId); } From c3b32cd2207432c1f0bf4ebb05c26acd1bea9706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:26:12 +0900 Subject: [PATCH 11/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20=EA=B8=88=EC=95=A1=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=84=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=82=B4=EB=B6=80=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#2?= =?UTF-8?q?516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/service/DeliveryOrderService.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index 89eea47185..ca4580bd55 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -3,6 +3,7 @@ import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; import static in.koreatech.koin.domain.order.order.model.OrderStatus.CONFIRMING; import static in.koreatech.koin.domain.order.order.model.OrderType.DELIVERY; +import static in.koreatech.koin.global.code.ApiResponseCode.ORDER_PRICE_MISMATCH; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,12 +18,12 @@ import in.koreatech.koin.domain.order.order.repository.OrderRepository; import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; -import in.koreatech.koin.domain.payment.model.domain.DeliveryPaymentInfo; import in.koreatech.koin.domain.payment.model.entity.Payment; import in.koreatech.koin.domain.payment.repository.PaymentRepository; import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.global.exception.CustomException; import lombok.RequiredArgsConstructor; @Service @@ -52,11 +53,10 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ int totalProductPrice = cart.calculateItemsAmount(); int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); int totalPrice = totalProductPrice + deliveryFee; - DeliveryPaymentInfo.of( - request.phoneNumber(), request.address(), request.addressDetail(), request.longitude(), request.latitude(), - request.toOwner(), request.toRider(), request.provideCutlery(), request.totalMenuPrice(), - request.deliveryTip(), request.totalAmount() - ).validatePrice(totalProductPrice, deliveryFee, totalPrice); + validatePrice( + request.totalMenuPrice(), request.deliveryTip(), request.totalAmount(), + totalProductPrice, deliveryFee, totalPrice + ); Order order = createOrder(user, orderableShop, request, totalProductPrice, deliveryFee, totalPrice); orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); @@ -69,6 +69,15 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ return OrderCreateResponse.of(order, payment); } + private void validatePrice(int requestedMenuPrice, int requestedDeliveryTip, int requestedTotalAmount, + int totalProductPrice, int deliveryFee, int totalPrice) { + if (requestedMenuPrice != totalProductPrice + || requestedDeliveryTip != deliveryFee + || requestedTotalAmount != totalPrice) { + throw CustomException.of(ORDER_PRICE_MISMATCH); + } + } + private Order createOrder(User user, OrderableShop orderableShop, DeliveryOrderRequest request, int totalProductPrice, int deliveryFee, int totalPrice) { Shop shop = orderableShop.getShop(); From b6d0b5f4c0d05e10144351fd43df0794f43cf5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:27:54 +0900 Subject: [PATCH 12/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20=EA=B8=88=EC=95=A1=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/service/DeliveryOrderService.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index ca4580bd55..43880225f7 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -54,8 +54,12 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); int totalPrice = totalProductPrice + deliveryFee; validatePrice( - request.totalMenuPrice(), request.deliveryTip(), request.totalAmount(), - totalProductPrice, deliveryFee, totalPrice + request.totalMenuPrice(), + request.deliveryTip(), + request.totalAmount(), + totalProductPrice, + deliveryFee, + totalPrice ); Order order = createOrder(user, orderableShop, request, totalProductPrice, deliveryFee, totalPrice); @@ -69,11 +73,15 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ return OrderCreateResponse.of(order, payment); } - private void validatePrice(int requestedMenuPrice, int requestedDeliveryTip, int requestedTotalAmount, - int totalProductPrice, int deliveryFee, int totalPrice) { - if (requestedMenuPrice != totalProductPrice - || requestedDeliveryTip != deliveryFee - || requestedTotalAmount != totalPrice) { + private void validatePrice( + int requestedMenuPrice, + int requestedDeliveryTip, + int requestedTotalAmount, + int totalProductPrice, + int deliveryFee, + int totalPrice + ) { + if (requestedMenuPrice != totalProductPrice || requestedDeliveryTip != deliveryFee || requestedTotalAmount != totalPrice) { throw CustomException.of(ORDER_PRICE_MISMATCH); } } From 21c888665f21d83e3bbbe292506d3f39e75ef89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:29:23 +0900 Subject: [PATCH 13/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EC=A3=BC=EB=AC=B8=20=EC=83=9D=EC=84=B1=EC=9D=84=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=A0=95=EC=A0=81=20=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/order/order/model/Order.java | 48 +++++++++++++++++ .../order/service/DeliveryOrderService.java | 53 ++++++------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java index 76917b2aa9..7f64f75791 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java @@ -8,6 +8,7 @@ import static java.lang.Boolean.FALSE; import static lombok.AccessLevel.PROTECTED; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -16,6 +17,7 @@ import in.koreatech.koin.common.model.BaseEntity; import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; +import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.global.code.ApiResponseCode; import in.koreatech.koin.global.exception.CustomException; @@ -166,6 +168,52 @@ public Order( this.orderMenus = orderMenus; } + public static Order createDelivery( + User user, + OrderableShop orderableShop, + String orderNumber, + String phoneNumber, + String address, + String addressDetail, + BigDecimal longitude, + BigDecimal latitude, + String toOwner, + String toRider, + Boolean provideCutlery, + int totalProductPrice, + int deliveryFee, + int totalPrice + ) { + Shop shop = orderableShop.getShop(); + Order order = Order.builder() + .orderNumber(orderNumber) + .orderType(OrderType.DELIVERY) + .status(CONFIRMING) + .orderableShopAddress(shop.getAddress()) + .orderableShopAddressDetail(shop.getAddressDetail()) + .orderableShopName(shop.getName()) + .phoneNumber(phoneNumber) + .totalProductPrice(totalProductPrice) + .discountAmount(0) + .totalPrice(totalPrice) + .orderableShop(orderableShop) + .user(user) + .isDeleted(false) + .build(); + order.setOrderDelivery(OrderDelivery.builder() + .order(order) + .address(address) + .addressDetail(addressDetail) + .longitude(longitude) + .latitude(latitude) + .toOwner(toOwner) + .toRider(toRider) + .provideCutlery(provideCutlery) + .deliveryTip(deliveryFee) + .build()); + return order; + } + public void setOrderDelivery(OrderDelivery orderDelivery) { this.orderDelivery = orderDelivery; } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index 43880225f7..700debcae5 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -1,8 +1,6 @@ package in.koreatech.koin.domain.order.order.service; import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; -import static in.koreatech.koin.domain.order.order.model.OrderStatus.CONFIRMING; -import static in.koreatech.koin.domain.order.order.model.OrderType.DELIVERY; import static in.koreatech.koin.global.code.ApiResponseCode.ORDER_PRICE_MISMATCH; import org.springframework.stereotype.Service; @@ -14,13 +12,11 @@ import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest; import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.model.Order; -import in.koreatech.koin.domain.order.order.model.OrderDelivery; import in.koreatech.koin.domain.order.order.repository.OrderRepository; import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; import in.koreatech.koin.domain.payment.model.entity.Payment; import in.koreatech.koin.domain.payment.repository.PaymentRepository; -import in.koreatech.koin.domain.shop.model.shop.Shop; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.global.exception.CustomException; @@ -62,7 +58,22 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ totalPrice ); - Order order = createOrder(user, orderableShop, request, totalProductPrice, deliveryFee, totalPrice); + Order order = Order.createDelivery( + user, + orderableShop, + orderNumberGenerator.generate(), + request.phoneNumber(), + request.address(), + request.addressDetail(), + request.longitude(), + request.latitude(), + request.toOwner(), + request.toRider(), + request.provideCutlery(), + totalProductPrice, + deliveryFee, + totalPrice + ); orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); orderRepository.save(order); @@ -85,36 +96,4 @@ private void validatePrice( throw CustomException.of(ORDER_PRICE_MISMATCH); } } - - private Order createOrder(User user, OrderableShop orderableShop, DeliveryOrderRequest request, - int totalProductPrice, int deliveryFee, int totalPrice) { - Shop shop = orderableShop.getShop(); - Order order = Order.builder() - .orderNumber(orderNumberGenerator.generate()) - .orderType(DELIVERY) - .status(CONFIRMING) - .orderableShopAddress(shop.getAddress()) - .orderableShopAddressDetail(shop.getAddressDetail()) - .orderableShopName(shop.getName()) - .phoneNumber(request.phoneNumber()) - .totalProductPrice(totalProductPrice) - .discountAmount(0) - .totalPrice(totalPrice) - .orderableShop(orderableShop) - .user(user) - .isDeleted(false) - .build(); - order.setOrderDelivery(OrderDelivery.builder() - .order(order) - .address(request.address()) - .addressDetail(request.addressDetail()) - .longitude(request.longitude()) - .latitude(request.latitude()) - .toOwner(request.toOwner()) - .toRider(request.toRider()) - .provideCutlery(request.provideCutlery()) - .deliveryTip(deliveryFee) - .build()); - return order; - } } From 71eebfcfead7d2cc52c797fd4ba6580088a4b8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:30:42 +0900 Subject: [PATCH 14/20] =?UTF-8?q?refactor:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/service/DeliveryOrderService.java | 18 ++------------ .../order/order/util/OrderPriceValidator.java | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index 700debcae5..8b5d4a18bc 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -1,7 +1,6 @@ package in.koreatech.koin.domain.order.order.service; import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; -import static in.koreatech.koin.global.code.ApiResponseCode.ORDER_PRICE_MISMATCH; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,13 +12,13 @@ import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.model.Order; import in.koreatech.koin.domain.order.order.repository.OrderRepository; +import in.koreatech.koin.domain.order.order.util.OrderPriceValidator; import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; import in.koreatech.koin.domain.payment.model.entity.Payment; import in.koreatech.koin.domain.payment.repository.PaymentRepository; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.repository.UserRepository; -import in.koreatech.koin.global.exception.CustomException; import lombok.RequiredArgsConstructor; @Service @@ -49,7 +48,7 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ int totalProductPrice = cart.calculateItemsAmount(); int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); int totalPrice = totalProductPrice + deliveryFee; - validatePrice( + OrderPriceValidator.validatePrice( request.totalMenuPrice(), request.deliveryTip(), request.totalAmount(), @@ -83,17 +82,4 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ return OrderCreateResponse.of(order, payment); } - - private void validatePrice( - int requestedMenuPrice, - int requestedDeliveryTip, - int requestedTotalAmount, - int totalProductPrice, - int deliveryFee, - int totalPrice - ) { - if (requestedMenuPrice != totalProductPrice || requestedDeliveryTip != deliveryFee || requestedTotalAmount != totalPrice) { - throw CustomException.of(ORDER_PRICE_MISMATCH); - } - } } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java b/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java new file mode 100644 index 0000000000..66b13c11e4 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java @@ -0,0 +1,24 @@ +package in.koreatech.koin.domain.order.order.util; + +import static in.koreatech.koin.global.code.ApiResponseCode.ORDER_PRICE_MISMATCH; +import static lombok.AccessLevel.PRIVATE; + +import in.koreatech.koin.global.exception.CustomException; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class OrderPriceValidator { + + public static void validatePrice( + int requestedMenuPrice, + int requestedDeliveryTip, + int requestedTotalAmount, + int totalProductPrice, + int deliveryFee, + int totalPrice + ) { + if (requestedMenuPrice != totalProductPrice || requestedDeliveryTip != deliveryFee || requestedTotalAmount != totalPrice) { + throw CustomException.of(ORDER_PRICE_MISMATCH); + } + } +} From a1540f6c8e1e4e163bf27546fd907955c710f8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:33:25 +0900 Subject: [PATCH 15/20] =?UTF-8?q?refactor:=20=EA=B3=84=EC=A2=8C=EC=9D=B4?= =?UTF-8?q?=EC=B2=B4=20Flyway=20DDL=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EC=98=88=EA=B8=88=EC=A3=BC=20=EC=BB=AC=EB=9F=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/payment/controller/PaymentApi.java | 2 +- .../payment/dto/response/PaymentResponse.java | 2 +- .../koin/domain/payment/model/entity/Payment.java | 7 ------- .../koreatech/koin/domain/shop/model/shop/Shop.java | 6 ------ .../db/migration/V21__allow_null_pg_order_id.sql | 2 ++ .../V21__support_direct_bank_transfer_orders.sql | 13 ------------- .../db/migration/V22__allow_null_payment_key.sql | 2 ++ .../V23__allow_null_payment_approved_at.sql | 2 ++ .../migration/V24__allow_null_payment_receipt.sql | 2 ++ .../db/migration/V25__add_payment_bank_snapshot.sql | 2 ++ .../V26__add_payment_account_number_snapshot.sql | 2 ++ 11 files changed, 14 insertions(+), 28 deletions(-) create mode 100644 src/main/resources/db/migration/V21__allow_null_pg_order_id.sql delete mode 100644 src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql create mode 100644 src/main/resources/db/migration/V22__allow_null_payment_key.sql create mode 100644 src/main/resources/db/migration/V23__allow_null_payment_approved_at.sql create mode 100644 src/main/resources/db/migration/V24__allow_null_payment_receipt.sql create mode 100644 src/main/resources/db/migration/V25__add_payment_bank_snapshot.sql create mode 100644 src/main/resources/db/migration/V26__add_payment_account_number_snapshot.sql diff --git a/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java b/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java index afef522d28..72660115c5 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java +++ b/src/main/java/in/koreatech/koin/domain/payment/controller/PaymentApi.java @@ -150,7 +150,7 @@ ResponseEntity cancelPayment( - `payment_method`: 결제 수단 - `estimated_at`: 예상 시각 (HH:mm) - `order_status`: 주문 상태 - - `bank_account`: 직접 계좌이체 주문 당시 은행명(bank), 계좌번호(account_number), 예금주(account_holder) + - `bank_account`: 은행명(bank)과 계좌번호(account_number) 스냅샷, 임시 예금주(account_holder) """ ) @GetMapping("/{paymentId}") diff --git a/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java b/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java index 2fcce71441..2ffb49c33e 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java +++ b/src/main/java/in/koreatech/koin/domain/payment/dto/response/PaymentResponse.java @@ -96,7 +96,7 @@ public record PaymentResponse( @Schema(description = "주문 상태", example = "COOKING", requiredMode = REQUIRED) String orderStatus, - @Schema(description = "직접 계좌이체 주문 당시 상점 계좌 정보", requiredMode = NOT_REQUIRED) + @Schema(description = "직접 계좌이체 주문 계좌 정보. 은행명과 계좌번호는 스냅샷", requiredMode = NOT_REQUIRED) BankAccountResponse bankAccount ) { diff --git a/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java b/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java index 907d0cc2de..3e15559c17 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java +++ b/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java @@ -82,10 +82,6 @@ public class Payment extends BaseEntity { @Column(name = "account_number", length = 20, updatable = false) private String accountNumber; - @Size(max = 100) - @Column(name = "account_holder", length = 100, updatable = false) - private String accountHolder; - @NotNull @Column(name = "is_deleted", nullable = false) private Boolean isDeleted = FALSE; @@ -107,7 +103,6 @@ public Payment( String receipt, String bank, String accountNumber, - String accountHolder, Boolean isDeleted, Order order ) { @@ -122,7 +117,6 @@ public Payment( this.receipt = receipt; this.bank = bank; this.accountNumber = accountNumber; - this.accountHolder = accountHolder; this.isDeleted = isDeleted; this.order = order; } @@ -142,7 +136,6 @@ public static Payment createBankTransfer(Order order) { .requestedAt(LocalDateTime.now()) .bank(shop.getBank()) .accountNumber(shop.getAccountNumber()) - .accountHolder(shop.getAccountHolder()) .isDeleted(false) .build(); } diff --git a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java index a7a5b0983f..6fbdbb1902 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java +++ b/src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java @@ -162,10 +162,6 @@ public class Shop extends BaseEntity { @Column(name = "account_number", length = 20) private String accountNumber; - @Size(max = 100) - @Column(name = "account_holder", length = 100) - private String accountHolder; - @Column(name = "introduction", columnDefinition = "text") private String introduction; @@ -191,7 +187,6 @@ private Shop( Integer hit, String bank, String accountNumber, - String accountHolder, ShopCategory shopMainCategory, ShopOperation shopOperation ) { @@ -212,7 +207,6 @@ private Shop( this.hit = hit; this.bank = bank; this.accountNumber = accountNumber; - this.accountHolder = accountHolder; this.shopMainCategory = shopMainCategory; this.shopOperation = shopOperation; } diff --git a/src/main/resources/db/migration/V21__allow_null_pg_order_id.sql b/src/main/resources/db/migration/V21__allow_null_pg_order_id.sql new file mode 100644 index 0000000000..0279b8875f --- /dev/null +++ b/src/main/resources/db/migration/V21__allow_null_pg_order_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE `order_v2` + MODIFY COLUMN `pg_order_id` VARCHAR(64) NULL COMMENT 'PG 주문 ID. 직접 계좌이체 주문은 NULL'; diff --git a/src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql b/src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql deleted file mode 100644 index c9734167a3..0000000000 --- a/src/main/resources/db/migration/V21__support_direct_bank_transfer_orders.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE `shops` - ADD COLUMN `account_holder` VARCHAR(100) NULL COMMENT '예금주'; - -ALTER TABLE `order_v2` - MODIFY COLUMN `pg_order_id` VARCHAR(64) NULL COMMENT 'PG 주문 ID. 직접 계좌이체 주문은 NULL'; - -ALTER TABLE `payment_v2` - MODIFY COLUMN `payment_key` VARCHAR(200) NULL COMMENT 'PG 결제 키', - MODIFY COLUMN `approved_at` TIMESTAMP NULL DEFAULT NULL COMMENT '결제 승인 일시', - MODIFY COLUMN `receipt` TEXT NULL COMMENT 'PG 영수증', - ADD COLUMN `bank` VARCHAR(10) NULL COMMENT '주문 당시 은행명', - ADD COLUMN `account_number` VARCHAR(20) NULL COMMENT '주문 당시 계좌번호', - ADD COLUMN `account_holder` VARCHAR(100) NULL COMMENT '주문 당시 예금주'; diff --git a/src/main/resources/db/migration/V22__allow_null_payment_key.sql b/src/main/resources/db/migration/V22__allow_null_payment_key.sql new file mode 100644 index 0000000000..734b6216d0 --- /dev/null +++ b/src/main/resources/db/migration/V22__allow_null_payment_key.sql @@ -0,0 +1,2 @@ +ALTER TABLE `payment_v2` + MODIFY COLUMN `payment_key` VARCHAR(200) NULL COMMENT 'PG 결제 키'; diff --git a/src/main/resources/db/migration/V23__allow_null_payment_approved_at.sql b/src/main/resources/db/migration/V23__allow_null_payment_approved_at.sql new file mode 100644 index 0000000000..2387b06fb7 --- /dev/null +++ b/src/main/resources/db/migration/V23__allow_null_payment_approved_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE `payment_v2` + MODIFY COLUMN `approved_at` TIMESTAMP NULL DEFAULT NULL COMMENT '결제 승인 일시'; diff --git a/src/main/resources/db/migration/V24__allow_null_payment_receipt.sql b/src/main/resources/db/migration/V24__allow_null_payment_receipt.sql new file mode 100644 index 0000000000..4ddf29fa60 --- /dev/null +++ b/src/main/resources/db/migration/V24__allow_null_payment_receipt.sql @@ -0,0 +1,2 @@ +ALTER TABLE `payment_v2` + MODIFY COLUMN `receipt` TEXT NULL COMMENT 'PG 영수증'; diff --git a/src/main/resources/db/migration/V25__add_payment_bank_snapshot.sql b/src/main/resources/db/migration/V25__add_payment_bank_snapshot.sql new file mode 100644 index 0000000000..76bc4e9086 --- /dev/null +++ b/src/main/resources/db/migration/V25__add_payment_bank_snapshot.sql @@ -0,0 +1,2 @@ +ALTER TABLE `payment_v2` + ADD COLUMN `bank` VARCHAR(10) NULL COMMENT '주문 계좌 은행명 스냅샷'; diff --git a/src/main/resources/db/migration/V26__add_payment_account_number_snapshot.sql b/src/main/resources/db/migration/V26__add_payment_account_number_snapshot.sql new file mode 100644 index 0000000000..de38f7ef7b --- /dev/null +++ b/src/main/resources/db/migration/V26__add_payment_account_number_snapshot.sql @@ -0,0 +1,2 @@ +ALTER TABLE `payment_v2` + ADD COLUMN `account_number` VARCHAR(20) NULL COMMENT '주문 계좌번호 스냅샷'; From e7ae0d15e513420b8d2bbfe47f9f7cafee95c733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:36:08 +0900 Subject: [PATCH 16/20] =?UTF-8?q?refactor:=20=EC=A3=BC=EB=AC=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=B7=A8=EC=86=8C=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=EC=9D=84=20Clock=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=EB=8B=AC=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/order/order/model/Order.java | 4 ++-- .../order/order/service/DeliveryOrderService.java | 7 ++++++- .../koin/domain/payment/model/entity/Payment.java | 8 ++++---- .../domain/payment/service/PaymentCancelService.java | 10 +++++++--- .../domain/order/service/OwnerOrderServiceTest.java | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java index 7f64f75791..66c9993af6 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java @@ -301,10 +301,10 @@ public LocalDateTime getEstimatedAt() { return null; } - public void cancel(String cancelReason) { + public void cancel(String cancelReason, LocalDateTime canceledAt) { this.status = CANCELED; this.canceledReason = cancelReason; - this.canceledAt = LocalDateTime.now(); + this.canceledAt = canceledAt; } public void delivered() { diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java index 8b5d4a18bc..203c5b792c 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java @@ -2,6 +2,9 @@ import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; +import java.time.Clock; +import java.time.LocalDateTime; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +35,7 @@ public class DeliveryOrderService { private final OrderMenuItemsMapper orderMenuItemsMapper; private final OrderNumberGenerator orderNumberGenerator; private final AddressValidator addressValidator; + private final Clock clock; @Transactional public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequest request) { @@ -76,7 +80,8 @@ public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequ orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); orderRepository.save(order); - Payment payment = Payment.createBankTransfer(order); + LocalDateTime now = LocalDateTime.now(clock); + Payment payment = Payment.createBankTransfer(order, now); paymentRepository.save(payment); cartRepository.delete(cart); diff --git a/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java b/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java index 3e15559c17..23e47e5b3f 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java +++ b/src/main/java/in/koreatech/koin/domain/payment/model/entity/Payment.java @@ -121,7 +121,7 @@ public Payment( this.order = order; } - public static Payment createBankTransfer(Order order) { + public static Payment createBankTransfer(Order order, LocalDateTime requestedAt) { Shop shop = order.getOrderableShop().getShop(); String description = order.getOrderMenus().get(0).getMenuName(); if (order.getOrderMenus().size() > 1) { @@ -133,7 +133,7 @@ public static Payment createBankTransfer(Order order) { .paymentStatus(PaymentStatus.WAITING_FOR_DEPOSIT) .paymentMethod(PaymentMethod.ACCOUNT_TRANSFER) .description(description) - .requestedAt(LocalDateTime.now()) + .requestedAt(requestedAt) .bank(shop.getBank()) .accountNumber(shop.getAccountNumber()) .isDeleted(false) @@ -150,8 +150,8 @@ public void validateUserIdMatches(Integer userId) { } } - public void cancel(String reason) { + public void cancel(String reason, LocalDateTime canceledAt) { this.paymentStatus = CANCELED; - this.order.cancel(reason); + this.order.cancel(reason, canceledAt); } } diff --git a/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java b/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java index c49fa4335e..66b521cebb 100644 --- a/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java +++ b/src/main/java/in/koreatech/koin/domain/payment/service/PaymentCancelService.java @@ -3,6 +3,7 @@ import static in.koreatech.koin.global.code.ApiResponseCode.PAYMENT_ALREADY_CANCELED; import static in.koreatech.koin.global.code.ApiResponseCode.PAYMENT_CANCEL_ERROR; +import java.time.Clock; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -34,6 +35,7 @@ public class PaymentCancelService { private final PaymentGatewayService paymentGatewayService; private final PaymentIdempotencyKeyService paymentIdempotencyKeyService; private final PaymentCancelMapper paymentCancelMapper; + private final Clock clock; @Transactional public PaymentCancelResponse cancelPayment(User user, Integer paymentId, PaymentCancelInfo paymentCancelInfo) { @@ -61,7 +63,8 @@ private List cancel(Payment payment, String cancelReason) { cancelReason, paymentIdempotencyKey); validatePaymentIsCanceled(pgResponse.status()); - payment.cancel(cancelReason); + LocalDateTime now = LocalDateTime.now(clock); + payment.cancel(cancelReason, now); List paymentCancels = paymentCancelMapper.toEntity(payment, pgResponse); paymentCancelRepository.saveAll(paymentCancels); @@ -70,13 +73,14 @@ private List cancel(Payment payment, String cancelReason) { private List cancelBankTransferOrder(Payment payment, String cancelReason) { payment.getOrder().requireStatusChangeableTo(OrderStatus.CANCELED); - payment.cancel(cancelReason); + LocalDateTime now = LocalDateTime.now(clock); + payment.cancel(cancelReason, now); PaymentCancel paymentCancel = PaymentCancel.builder() .transactionKey(UUID.randomUUID().toString()) .cancelReason(cancelReason) // 입금과 환불은 앱에서 확인하지 않으므로 자동 환불 금액은 0원이다. .cancelAmount(0) - .canceledAt(LocalDateTime.now()) + .canceledAt(now) .isDeleted(false) .payment(payment) .build(); diff --git a/src/test/java/in/koreatech/koin/unit/domain/order/service/OwnerOrderServiceTest.java b/src/test/java/in/koreatech/koin/unit/domain/order/service/OwnerOrderServiceTest.java index 27e55fe8c5..71508508d7 100644 --- a/src/test/java/in/koreatech/koin/unit/domain/order/service/OwnerOrderServiceTest.java +++ b/src/test/java/in/koreatech/koin/unit/domain/order/service/OwnerOrderServiceTest.java @@ -362,7 +362,7 @@ class GetOrder { @DisplayName("반려된 주문은 반려 일시와 사유를 반환한다") void 반려된_주문은_반려_일시와_사유를_반환한다() { Order order = OrderFixture.배달_주문(1, "A1B2C3D4E5", OrderStatus.CONFIRMING, orderableShop, customer); - order.cancel("재료 소진"); + order.cancel("재료 소진", LocalDateTime.of(2026, 10, 27, 12, 0)); when(orderableShopRepository.getById(ORDERABLE_SHOP_ID)).thenReturn(orderableShop); when(orderRepository.findByIdAndOrderableShopId(1, ORDERABLE_SHOP_ID)).thenReturn(Optional.of(order)); From 66e055861c5c0cf219d8c18d7cd0497438ab4a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:38:13 +0900 Subject: [PATCH 17/20] =?UTF-8?q?docs:=20=EB=B0=B0=EB=8B=AC=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20API=20=EC=84=A4=EB=AA=85=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20Swagger=20=EC=9D=91=EB=8B=B5=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/order/controller/OrderApi.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java index 5ff276699c..5d0cf25eae 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java @@ -1,6 +1,7 @@ package in.koreatech.koin.domain.order.order.controller; import static in.koreatech.koin.domain.user.model.UserType.STUDENT; +import static in.koreatech.koin.global.code.ApiResponseCode.*; import java.util.List; @@ -19,6 +20,7 @@ import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; import in.koreatech.koin.global.auth.Auth; +import in.koreatech.koin.global.code.ApiResponseCodes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -27,15 +29,31 @@ @RequestMapping("/order") public interface OrderApi { + @ApiResponseCodes({ + OK, + INVALID_REQUEST_BODY, + NOT_READABLE_HTTP_MESSAGE, + INVALID_ADDRESS_FORMAT, + SHOP_NOT_DELIVERABLE, + SHOP_CLOSED, + ORDER_AMOUNT_BELOW_MINIMUM, + MENU_SOLD_OUT, + SHOP_BANK_ACCOUNT_UNAVAILABLE, + ORDER_PRICE_MISMATCH, + UNAUTHORIZED_USER, + WITHDRAWN_USER, + FORBIDDEN_USER_TYPE, + FORBIDDEN_STUDENT, + NOT_FOUND_USER, + NOT_FOUND_CART, + NOT_FOUND_CART_ITEM + }) @Operation( summary = "계좌이체 배달 주문을 생성한다.", description = """ - 기존 배달 임시 결제 저장 API와 동일한 요청 바디를 사용한다. - PG 승인 및 임시 저장 없이 장바구니로 주문을 생성하고 장바구니를 비운다. - 계좌이체만 지원하며 앱에서 입금 여부를 확인하지 않는다. - 응답의 계좌와 amount로 입금 안내를 표시한다. - 이후 GET /payments/{paymentId}로 주문 및 계좌 정보를 재조회한다. - 중복 호출 또는 실패 후 재시도 전에는 주문 내역을 확인한다. + 로그인한 학생의 장바구니로 계좌이체 배달 주문을 생성한다. + 메뉴 금액과 배달비를 검증하고 주문 생성 후 장바구니를 비운다. + 주문 정보, 입금할 금액과 상점 계좌 정보를 반환한다. """ ) @PostMapping("/delivery") From ed6751bfd38373e19deded7be4756e499fbf08c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:42:08 +0900 Subject: [PATCH 18/20] =?UTF-8?q?feat:=20=ED=8F=AC=EC=9E=A5=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=88=EC=95=A1=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/domain/order/cart/model/Cart.java | 9 +++++ .../koin/domain/order/order/model/Order.java | 34 +++++++++++++++++++ .../order/order/util/OrderPriceValidator.java | 9 +++++ 3 files changed, 52 insertions(+) diff --git a/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java b/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java index 8ee2f4b5bc..61b83aab05 100644 --- a/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java +++ b/src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java @@ -137,6 +137,15 @@ public void validateDeliveryOrder() { cartMenuItems.forEach(item -> item.getOrderableShopMenu().validateSoldOut()); } + public void validateTakeoutOrder() { + if (cartMenuItems.isEmpty()) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_CART_ITEM); + } + validateOrderType(OrderType.TAKE_OUT); + orderableShop.requireShopOpen(); + cartMenuItems.forEach(item -> item.getOrderableShopMenu().validateSoldOut()); + } + public Integer getItemTypeCount() { return this.cartMenuItems.size(); } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java index 66c9993af6..eb125e0108 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/model/Order.java @@ -214,6 +214,40 @@ public static Order createDelivery( return order; } + public static Order createTakeout( + User user, + OrderableShop orderableShop, + String orderNumber, + String phoneNumber, + String toOwner, + Boolean provideCutlery, + int totalProductPrice, + int totalPrice + ) { + Shop shop = orderableShop.getShop(); + Order order = Order.builder() + .orderNumber(orderNumber) + .orderType(OrderType.TAKE_OUT) + .status(CONFIRMING) + .orderableShopAddress(shop.getAddress()) + .orderableShopAddressDetail(shop.getAddressDetail()) + .orderableShopName(shop.getName()) + .phoneNumber(phoneNumber) + .totalProductPrice(totalProductPrice) + .discountAmount(0) + .totalPrice(totalPrice) + .orderableShop(orderableShop) + .user(user) + .isDeleted(false) + .build(); + order.setOrderTakeout(OrderTakeout.builder() + .order(order) + .toOwner(toOwner) + .provideCutlery(provideCutlery) + .build()); + return order; + } + public void setOrderDelivery(OrderDelivery orderDelivery) { this.orderDelivery = orderDelivery; } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java b/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java index 66b13c11e4..42cd62b590 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/util/OrderPriceValidator.java @@ -9,6 +9,15 @@ @NoArgsConstructor(access = PRIVATE) public class OrderPriceValidator { + public static void validatePrice( + int requestedMenuPrice, + int requestedTotalAmount, + int totalProductPrice, + int totalPrice + ) { + validatePrice(requestedMenuPrice, 0, requestedTotalAmount, totalProductPrice, 0, totalPrice); + } + public static void validatePrice( int requestedMenuPrice, int requestedDeliveryTip, From 260a70cc6cbf9caed1faaf37a30b1781ebc22199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:42:24 +0900 Subject: [PATCH 19/20] =?UTF-8?q?feat:=20=EA=B3=84=EC=A2=8C=EC=9D=B4?= =?UTF-8?q?=EC=B2=B4=20=ED=8F=AC=EC=9E=A5=20=EC=A3=BC=EB=AC=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20API=20=EA=B5=AC=ED=98=84=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/order/controller/OrderApi.java | 32 ++++++++ .../order/controller/OrderController.java | 12 +++ .../dto/request/TakeoutOrderRequest.java | 42 +++++++++++ .../order/service/TakeoutOrderService.java | 73 +++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/order/order/dto/request/TakeoutOrderRequest.java create mode 100644 src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java index 5d0cf25eae..dfad1907a6 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderApi.java @@ -16,6 +16,7 @@ import in.koreatech.koin.domain.order.order.dto.request.OrderSearchPeriodCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderStatusCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderTypeCriteria; +import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest; import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse; import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; @@ -62,6 +63,37 @@ ResponseEntity createDeliveryOrder( @Auth(permit = {STUDENT}) Integer userId ); + @ApiResponseCodes({ + OK, + INVALID_REQUEST_BODY, + NOT_READABLE_HTTP_MESSAGE, + SHOP_NOT_TAKEOUT_AVAILABLE, + SHOP_CLOSED, + MENU_SOLD_OUT, + SHOP_BANK_ACCOUNT_UNAVAILABLE, + ORDER_PRICE_MISMATCH, + UNAUTHORIZED_USER, + WITHDRAWN_USER, + FORBIDDEN_USER_TYPE, + FORBIDDEN_STUDENT, + NOT_FOUND_USER, + NOT_FOUND_CART, + NOT_FOUND_CART_ITEM + }) + @Operation( + summary = "계좌이체 포장 주문을 생성한다.", + description = """ + 로그인한 학생의 장바구니로 계좌이체 포장 주문을 생성한다. + 메뉴 금액과 총 결제 금액을 검증하고 주문 생성 후 장바구니를 비운다. + 배달비는 부과하지 않으며 주문 정보, 입금할 금액과 상점 계좌 정보를 반환한다. + """ + ) + @PostMapping("/takeout") + ResponseEntity createTakeoutOrder( + @RequestBody @Valid TakeoutOrderRequest request, + @Auth(permit = {STUDENT}) Integer userId + ); + @Operation( summary = "주문 이력을 조회한다.", description = """ diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java index 400a957a35..53d986b12d 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java @@ -17,11 +17,13 @@ import in.koreatech.koin.domain.order.order.dto.request.OrderSearchPeriodCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderStatusCriteria; import in.koreatech.koin.domain.order.order.dto.request.OrderTypeCriteria; +import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest; import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse; import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; import in.koreatech.koin.domain.order.order.service.DeliveryOrderService; import in.koreatech.koin.domain.order.order.service.OrderService; +import in.koreatech.koin.domain.order.order.service.TakeoutOrderService; import in.koreatech.koin.global.auth.Auth; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -33,6 +35,7 @@ public class OrderController implements OrderApi { private final OrderService orderService; private final DeliveryOrderService deliveryOrderService; + private final TakeoutOrderService takeoutOrderService; @PostMapping("/delivery") public ResponseEntity createDeliveryOrder( @@ -43,6 +46,15 @@ public ResponseEntity createDeliveryOrder( return ResponseEntity.ok(response); } + @PostMapping("/takeout") + public ResponseEntity createTakeoutOrder( + @RequestBody @Valid TakeoutOrderRequest request, + @Auth(permit = {STUDENT}) Integer userId + ) { + OrderCreateResponse response = takeoutOrderService.createTakeoutOrder(userId, request); + return ResponseEntity.ok(response); + } + @GetMapping public ResponseEntity getOrders( @RequestParam(name = "page", required = false, defaultValue = "1") Integer page, diff --git a/src/main/java/in/koreatech/koin/domain/order/order/dto/request/TakeoutOrderRequest.java b/src/main/java/in/koreatech/koin/domain/order/order/dto/request/TakeoutOrderRequest.java new file mode 100644 index 0000000000..94f2255931 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/order/order/dto/request/TakeoutOrderRequest.java @@ -0,0 +1,42 @@ +package in.koreatech.koin.domain.order.order.dto.request; + +import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.PositiveOrZero; +import jakarta.validation.constraints.Size; + +@JsonNaming(value = SnakeCaseStrategy.class) +public record TakeoutOrderRequest( + @Schema(description = "연락처", example = "01012345678", requiredMode = REQUIRED) + @NotBlank(message = "연락처는 필수 입력사항입니다.") + @Pattern(regexp = "^\\d{11}$", message = "전화번호 형식이 올바르지 않습니다. 11자리 숫자로 입력해 주세요.") + String phoneNumber, + + @Schema(description = "사장님에게", example = "리뷰 이벤트 감사합니다.", requiredMode = NOT_REQUIRED) + @Size(max = 50, message = "사장님 요청 사항은 50자 이하여야 합니다.") + String toOwner, + + @Schema(description = "수저, 포크 수령 여부", example = "true", requiredMode = REQUIRED) + @NotNull(message = "수저, 포크 수령 여부는 필수 입력사항입니다.") + Boolean provideCutlery, + + @Schema(description = "메뉴 총 금액", example = "1234", requiredMode = REQUIRED) + @NotNull(message = "메뉴 총 금액은 필수 입력사항입니다.") + @PositiveOrZero(message = "금액은 0 이상이어야 합니다.") + Integer totalMenuPrice, + + @Schema(description = "결제 금액", example = "10000", requiredMode = REQUIRED) + @NotNull(message = "결제 금액은 필수 입력사항입니다.") + @PositiveOrZero(message = "금액은 0 이상이어야 합니다.") + Integer totalAmount +) { + +} diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java new file mode 100644 index 0000000000..6ad3dbcc27 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java @@ -0,0 +1,73 @@ +package in.koreatech.koin.domain.order.order.service; + +import java.time.Clock; +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.domain.order.cart.model.Cart; +import in.koreatech.koin.domain.order.cart.repository.CartRepository; +import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest; +import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; +import in.koreatech.koin.domain.order.order.model.Order; +import in.koreatech.koin.domain.order.order.repository.OrderRepository; +import in.koreatech.koin.domain.order.order.util.OrderPriceValidator; +import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; +import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; +import in.koreatech.koin.domain.payment.model.entity.Payment; +import in.koreatech.koin.domain.payment.repository.PaymentRepository; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class TakeoutOrderService { + + private final UserRepository userRepository; + private final CartRepository cartRepository; + private final OrderRepository orderRepository; + private final PaymentRepository paymentRepository; + private final OrderMenuItemsMapper orderMenuItemsMapper; + private final OrderNumberGenerator orderNumberGenerator; + private final Clock clock; + + @Transactional + public OrderCreateResponse createTakeoutOrder(Integer userId, TakeoutOrderRequest request) { + User user = userRepository.getById(userId); + Cart cart = cartRepository.getCartByUserId(userId); + cart.validateTakeoutOrder(); + OrderableShop orderableShop = cart.getOrderableShop(); + orderableShop.getShop().validateBankAccount(); + + int totalProductPrice = cart.calculateItemsAmount(); + int totalPrice = totalProductPrice; + OrderPriceValidator.validatePrice( + request.totalMenuPrice(), + request.totalAmount(), + totalProductPrice, + totalPrice + ); + + Order order = Order.createTakeout( + user, + orderableShop, + orderNumberGenerator.generate(), + request.phoneNumber(), + request.toOwner(), + request.provideCutlery(), + totalProductPrice, + totalPrice + ); + orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); + orderRepository.save(order); + + LocalDateTime now = LocalDateTime.now(clock); + Payment payment = Payment.createBankTransfer(order, now); + paymentRepository.save(payment); + cartRepository.delete(cart); + + return OrderCreateResponse.of(order, payment); + } +} From fdda007e8139b8e63caf914b9981f35720fa6970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=A0=EA=B4=80=EA=B7=9C?= Date: Tue, 27 Oct 2026 22:43:17 +0900 Subject: [PATCH 20/20] =?UTF-8?q?refactor:=20=EB=B0=B0=EB=8B=AC=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=AC=EC=9E=A5=20=EC=A3=BC=EB=AC=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20OrderService=EB=A1=9C=20=ED=86=B5=ED=95=A9?= =?UTF-8?q?=20(#2516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/controller/OrderController.java | 8 +- .../order/service/DeliveryOrderService.java | 90 --------------- .../order/order/service/OrderService.java | 103 ++++++++++++++++++ .../order/service/TakeoutOrderService.java | 73 ------------- 4 files changed, 105 insertions(+), 169 deletions(-) delete mode 100644 src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java delete mode 100644 src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java diff --git a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java index 53d986b12d..b85784c39b 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/controller/OrderController.java @@ -21,9 +21,7 @@ import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse; import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; -import in.koreatech.koin.domain.order.order.service.DeliveryOrderService; import in.koreatech.koin.domain.order.order.service.OrderService; -import in.koreatech.koin.domain.order.order.service.TakeoutOrderService; import in.koreatech.koin.global.auth.Auth; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -34,15 +32,13 @@ public class OrderController implements OrderApi { private final OrderService orderService; - private final DeliveryOrderService deliveryOrderService; - private final TakeoutOrderService takeoutOrderService; @PostMapping("/delivery") public ResponseEntity createDeliveryOrder( @RequestBody @Valid DeliveryOrderRequest request, @Auth(permit = {STUDENT}) Integer userId ) { - OrderCreateResponse response = deliveryOrderService.createDeliveryOrder(userId, request); + OrderCreateResponse response = orderService.createDeliveryOrder(userId, request); return ResponseEntity.ok(response); } @@ -51,7 +47,7 @@ public ResponseEntity createTakeoutOrder( @RequestBody @Valid TakeoutOrderRequest request, @Auth(permit = {STUDENT}) Integer userId ) { - OrderCreateResponse response = takeoutOrderService.createTakeoutOrder(userId, request); + OrderCreateResponse response = orderService.createTakeoutOrder(userId, request); return ResponseEntity.ok(response); } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java deleted file mode 100644 index 203c5b792c..0000000000 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/DeliveryOrderService.java +++ /dev/null @@ -1,90 +0,0 @@ -package in.koreatech.koin.domain.order.order.service; - -import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; - -import java.time.Clock; -import java.time.LocalDateTime; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.domain.address.service.AddressValidator; -import in.koreatech.koin.domain.order.cart.model.Cart; -import in.koreatech.koin.domain.order.cart.repository.CartRepository; -import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest; -import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; -import in.koreatech.koin.domain.order.order.model.Order; -import in.koreatech.koin.domain.order.order.repository.OrderRepository; -import in.koreatech.koin.domain.order.order.util.OrderPriceValidator; -import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; -import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; -import in.koreatech.koin.domain.payment.model.entity.Payment; -import in.koreatech.koin.domain.payment.repository.PaymentRepository; -import in.koreatech.koin.domain.user.model.User; -import in.koreatech.koin.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class DeliveryOrderService { - - private final UserRepository userRepository; - private final CartRepository cartRepository; - private final OrderRepository orderRepository; - private final PaymentRepository paymentRepository; - private final OrderMenuItemsMapper orderMenuItemsMapper; - private final OrderNumberGenerator orderNumberGenerator; - private final AddressValidator addressValidator; - private final Clock clock; - - @Transactional - public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequest request) { - User user = userRepository.getById(userId); - if (request.deliveryType() == OFF_CAMPUS) { - addressValidator.validateAddress(request.address()); - } - - Cart cart = cartRepository.getCartByUserId(userId); - cart.validateDeliveryOrder(); - OrderableShop orderableShop = cart.getOrderableShop(); - orderableShop.getShop().validateBankAccount(); - - int totalProductPrice = cart.calculateItemsAmount(); - int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); - int totalPrice = totalProductPrice + deliveryFee; - OrderPriceValidator.validatePrice( - request.totalMenuPrice(), - request.deliveryTip(), - request.totalAmount(), - totalProductPrice, - deliveryFee, - totalPrice - ); - - Order order = Order.createDelivery( - user, - orderableShop, - orderNumberGenerator.generate(), - request.phoneNumber(), - request.address(), - request.addressDetail(), - request.longitude(), - request.latitude(), - request.toOwner(), - request.toRider(), - request.provideCutlery(), - totalProductPrice, - deliveryFee, - totalPrice - ); - orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); - orderRepository.save(order); - - LocalDateTime now = LocalDateTime.now(clock); - Payment payment = Payment.createBankTransfer(order, now); - paymentRepository.save(payment); - cartRepository.delete(cart); - - return OrderCreateResponse.of(order, payment); - } -} diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/OrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/OrderService.java index 0fd8fe3350..0bf8e7d35f 100644 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/OrderService.java +++ b/src/main/java/in/koreatech/koin/domain/order/order/service/OrderService.java @@ -1,13 +1,23 @@ package in.koreatech.koin.domain.order.order.service; +import static in.koreatech.koin.domain.order.delivery.model.AddressType.OFF_CAMPUS; + +import java.time.Clock; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import in.koreatech.koin.domain.address.service.AddressValidator; +import in.koreatech.koin.domain.order.cart.model.Cart; +import in.koreatech.koin.domain.order.cart.repository.CartRepository; +import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest; import in.koreatech.koin.domain.order.order.dto.request.OrderSearchCondition; +import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest; import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse; +import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse; import in.koreatech.koin.domain.order.order.model.Order; import in.koreatech.koin.domain.order.order.model.OrderInfo; @@ -15,6 +25,9 @@ import in.koreatech.koin.domain.order.order.model.OrderStatus; import in.koreatech.koin.domain.order.order.repository.OrderRepository; import in.koreatech.koin.domain.order.order.repository.OrderSearchQueryRepository; +import in.koreatech.koin.domain.order.order.util.OrderPriceValidator; +import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; +import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; import in.koreatech.koin.domain.payment.model.entity.Payment; import in.koreatech.koin.domain.payment.repository.PaymentRepository; import in.koreatech.koin.domain.user.model.User; @@ -30,6 +43,84 @@ public class OrderService { private final UserRepository userRepository; private final PaymentRepository paymentRepository; private final OrderRepository orderRepository; + private final CartRepository cartRepository; + private final OrderMenuItemsMapper orderMenuItemsMapper; + private final OrderNumberGenerator orderNumberGenerator; + private final AddressValidator addressValidator; + private final Clock clock; + + @Transactional + public OrderCreateResponse createDeliveryOrder(Integer userId, DeliveryOrderRequest request) { + User user = userRepository.getById(userId); + if (request.deliveryType() == OFF_CAMPUS) { + addressValidator.validateAddress(request.address()); + } + + Cart cart = cartRepository.getCartByUserId(userId); + cart.validateDeliveryOrder(); + OrderableShop orderableShop = cart.getOrderableShop(); + orderableShop.getShop().validateBankAccount(); + + int totalProductPrice = cart.calculateItemsAmount(); + int deliveryFee = orderableShop.calculateDeliveryFee(totalProductPrice); + int totalPrice = totalProductPrice + deliveryFee; + OrderPriceValidator.validatePrice( + request.totalMenuPrice(), + request.deliveryTip(), + request.totalAmount(), + totalProductPrice, + deliveryFee, + totalPrice + ); + + Order order = Order.createDelivery( + user, + orderableShop, + orderNumberGenerator.generate(), + request.phoneNumber(), + request.address(), + request.addressDetail(), + request.longitude(), + request.latitude(), + request.toOwner(), + request.toRider(), + request.provideCutlery(), + totalProductPrice, + deliveryFee, + totalPrice + ); + return saveOrder(order, cart); + } + + @Transactional + public OrderCreateResponse createTakeoutOrder(Integer userId, TakeoutOrderRequest request) { + User user = userRepository.getById(userId); + Cart cart = cartRepository.getCartByUserId(userId); + cart.validateTakeoutOrder(); + OrderableShop orderableShop = cart.getOrderableShop(); + orderableShop.getShop().validateBankAccount(); + + int totalProductPrice = cart.calculateItemsAmount(); + int totalPrice = totalProductPrice; + OrderPriceValidator.validatePrice( + request.totalMenuPrice(), + request.totalAmount(), + totalProductPrice, + totalPrice + ); + + Order order = Order.createTakeout( + user, + orderableShop, + orderNumberGenerator.generate(), + request.phoneNumber(), + request.toOwner(), + request.provideCutlery(), + totalProductPrice, + totalPrice + ); + return saveOrder(order, cart); + } public OrdersResponse getOrders(Integer userId, OrderSearchCondition orderSearchCondition) { OrderSearchCriteria orderSearchCriteria = OrderSearchCriteria.from(orderSearchCondition); @@ -49,4 +140,16 @@ public List getInprogressOrders(Integer userId) { }) .toList(); } + + private OrderCreateResponse saveOrder(Order order, Cart cart) { + orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); + orderRepository.save(order); + + LocalDateTime now = LocalDateTime.now(clock); + Payment payment = Payment.createBankTransfer(order, now); + paymentRepository.save(payment); + cartRepository.delete(cart); + + return OrderCreateResponse.of(order, payment); + } } diff --git a/src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java b/src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java deleted file mode 100644 index 6ad3dbcc27..0000000000 --- a/src/main/java/in/koreatech/koin/domain/order/order/service/TakeoutOrderService.java +++ /dev/null @@ -1,73 +0,0 @@ -package in.koreatech.koin.domain.order.order.service; - -import java.time.Clock; -import java.time.LocalDateTime; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import in.koreatech.koin.domain.order.cart.model.Cart; -import in.koreatech.koin.domain.order.cart.repository.CartRepository; -import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest; -import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse; -import in.koreatech.koin.domain.order.order.model.Order; -import in.koreatech.koin.domain.order.order.repository.OrderRepository; -import in.koreatech.koin.domain.order.order.util.OrderPriceValidator; -import in.koreatech.koin.domain.order.shop.model.entity.shop.OrderableShop; -import in.koreatech.koin.domain.payment.mapper.OrderMenuItemsMapper; -import in.koreatech.koin.domain.payment.model.entity.Payment; -import in.koreatech.koin.domain.payment.repository.PaymentRepository; -import in.koreatech.koin.domain.user.model.User; -import in.koreatech.koin.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class TakeoutOrderService { - - private final UserRepository userRepository; - private final CartRepository cartRepository; - private final OrderRepository orderRepository; - private final PaymentRepository paymentRepository; - private final OrderMenuItemsMapper orderMenuItemsMapper; - private final OrderNumberGenerator orderNumberGenerator; - private final Clock clock; - - @Transactional - public OrderCreateResponse createTakeoutOrder(Integer userId, TakeoutOrderRequest request) { - User user = userRepository.getById(userId); - Cart cart = cartRepository.getCartByUserId(userId); - cart.validateTakeoutOrder(); - OrderableShop orderableShop = cart.getOrderableShop(); - orderableShop.getShop().validateBankAccount(); - - int totalProductPrice = cart.calculateItemsAmount(); - int totalPrice = totalProductPrice; - OrderPriceValidator.validatePrice( - request.totalMenuPrice(), - request.totalAmount(), - totalProductPrice, - totalPrice - ); - - Order order = Order.createTakeout( - user, - orderableShop, - orderNumberGenerator.generate(), - request.phoneNumber(), - request.toOwner(), - request.provideCutlery(), - totalProductPrice, - totalPrice - ); - orderMenuItemsMapper.toOrderMenus(cart, order).forEach(order::addOrderMenu); - orderRepository.save(order); - - LocalDateTime now = LocalDateTime.now(clock); - Payment payment = Payment.createBankTransfer(order, now); - paymentRepository.save(payment); - cartRepository.delete(cart); - - return OrderCreateResponse.of(order, payment); - } -}