diff --git a/pom.xml b/pom.xml index d3b60f2..ca8e00d 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,13 @@ spring-boot-starter-aop + + + org.apache.commons + commons-lang3 + 3.4 + + com.google.firebase diff --git a/src/main/java/com/delfood/aop/AuthCheckAspect.java b/src/main/java/com/delfood/aop/AuthCheckAspect.java index 4bd00b7..3784c68 100644 --- a/src/main/java/com/delfood/aop/AuthCheckAspect.java +++ b/src/main/java/com/delfood/aop/AuthCheckAspect.java @@ -1,9 +1,16 @@ package com.delfood.aop; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Objects; import javax.servlet.http.HttpSession; +import org.apache.commons.codec.binary.StringUtils; import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.reflect.MethodSignature; +import org.codehaus.commons.compiler.util.StringUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -27,9 +34,8 @@ public class AuthCheckAspect { * 로그인되어있지 않을 시 해당 메서드 로직을 중지시킨 후 리턴한다. * @OwnerLoginCheck 해당 어노테이션이 적용된 메서드를 검사한다. * @author jun - * @param pjp - * @return 로그인시 SUCCESS, 비로그인시 NO_LOGIN - * @throws Throwable + * @param jp 조인포인트 + * @throws Throwable 발생 가능한 예외 */ @Before("@annotation(com.delfood.aop.OwnerLoginCheck)") public void ownerLoginCheck(JoinPoint jp) throws Throwable { @@ -49,25 +55,46 @@ public void ownerLoginCheck(JoinPoint jp) throws Throwable { * 세션에서 사장님 로그인을 체크 한다. * 그 후 입력받은 파라미터 값 중 매장 id를 검색하여 해당 매장이 접속한 사장님의 것인지 검사한다. * @author jun - * @param pjp - * @return 비로그인시 NO_LOGIN, 해당 매장의 사장이 아닐 시 UNAUTHORIZED, 권한이 있을 시 SUCCESS - * @throws Throwable + * @param jp 조인포인트 + * @throws Throwable 발새 가능한 예외 */ - @Before("@annotation(com.delfood.aop.OwnerShopCheck)") - public void ownerShopCheck(JoinPoint jp) throws Throwable { + @Before("@annotation(com.delfood.aop.OwnerShopCheck) && @annotation(ownerShopCheck)") + public void ownerShopCheck(JoinPoint jp, OwnerShopCheck ownerShopCheck) throws Throwable { log.debug("AOP - Owner Shop Check Started"); - HttpSession session = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest().getSession(); + HttpSession session = + ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest() + .getSession(); String ownerId = SessionUtil.getLoginOwnerId(session); - - if(ownerId == null) { + + if (ownerId == null) { log.debug("AOP - Owner Shop Check Result - NO_LOGIN"); throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {}; } Object[] args = jp.getArgs(); - Long shopId = (Long) args[0]; + + // 메소드 파라미터 추출 + MethodSignature signature = (MethodSignature) jp.getSignature(); + Method method = signature.getMethod(); + Parameter[] parameters = method.getParameters(); + + Long shopId = null; + + // 파라미터의 이름과 어노테이션의 value를 비교하여 검사 + for (int i = 0; i < parameters.length; i++) { + String parameterName = parameters[i].getName(); + if (StringUtils.equals(ownerShopCheck.value(), parameterName)) { + shopId = (Long) args[i]; + } + } + + // 어노테이션 value로 설정된 값과 같은 변수 이름이 없을 경우 예외처리 + if (Objects.isNull(shopId)) { + throw new IllegalArgumentException("OwnerShopCheck 어노테이션 설정이 잘못되었습니다. value와 변수 명을 일치시켜주세요."); + } + if (!shopService.isShopOwner(shopId, ownerId)) { log.debug("AOP - Owner Shop Check Result - UNAUTHORIZED"); @@ -78,9 +105,8 @@ public void ownerShopCheck(JoinPoint jp) throws Throwable { /** * 고객의 로그인을 체크한다. * @author jun - * @param pjp - * @return - * @throws Throwable + * @param jp 조인포인튼 + * @throws Throwable 발생 가능한 예외 */ @Before("@annotation(com.delfood.aop.MemberLoginCheck)") public void memberLoginCheck(JoinPoint jp) throws Throwable { @@ -93,4 +119,54 @@ public void memberLoginCheck(JoinPoint jp) throws Throwable { throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "NO_LOGIN") {}; } } + + /** + * 라이더 로그인을 체크한다. + * @author jun + * @param jp 조인포인트 + * @throws Throwable 발생 가능한 예외 설정 + */ + @Before("@annotation(com.delfood.aop.RiderLoginCheck)") + public void riderLoginCheck(JoinPoint jp) throws Throwable { + log.debug("AOP - Rider Login Check Started"); + + HttpSession session = + ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest() + .getSession(); + String riderId = SessionUtil.getLoginRiderId(session); + + if (Objects.isNull(riderId)) { + throw new HttpStatusCodeException(HttpStatus.UNAUTHORIZED, "RIDER_NO_LOGIN") {}; + } + } + + /** + * 공통 로그인 체크 AOP. + * 고객, 사장님, 라이더의 로그인 체크 기능을 하나로 모아두었다. + * @param jp 조인포인트 + * @throws Throwable 발생 가능한 예외 + */ + @Before("@annotation(com.delfood.aop.LoginCheck) && @ annotation(loginCheck)") + public void loginCheck(JoinPoint jp, LoginCheck loginCheck) throws Throwable { + log.debug("AOP - Login Check Started"); + + HttpSession session = + ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest() + .getSession(); + + if (LoginCheck.UserType.MEMBER.equals(loginCheck.type())) { + memberLoginCheck(jp); + } + + if (LoginCheck.UserType.OWNER.equals(loginCheck.type())) { + ownerLoginCheck(jp); + } + + if (LoginCheck.UserType.RIDER.equals(loginCheck.type())) { + riderLoginCheck(jp); + } + + + } + } diff --git a/src/main/java/com/delfood/aop/LoginCheck.java b/src/main/java/com/delfood/aop/LoginCheck.java new file mode 100644 index 0000000..3aa4d32 --- /dev/null +++ b/src/main/java/com/delfood/aop/LoginCheck.java @@ -0,0 +1,28 @@ +package com.delfood.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 로그인의 상태를 확인한다. + * 회원, 사장님, 라이더의 로그인 상태를 확인하여 로그인 되지 않았다면 예외를 발생시킨다. + * @author jun + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface LoginCheck { + + /** + * 로그인을 체크하고 싶은 유저의 로그인 타입. + * 회원(MEMBER), 사장님(OWNER), 라이더(RIDER)중 선택할 수 있다. + * @return + */ + UserType type(); + + public static enum UserType { + MEMBER, OWNER, RIDER + } +} diff --git a/src/main/java/com/delfood/aop/OwnerShopCheck.java b/src/main/java/com/delfood/aop/OwnerShopCheck.java index 0034c2a..1908239 100644 --- a/src/main/java/com/delfood/aop/OwnerShopCheck.java +++ b/src/main/java/com/delfood/aop/OwnerShopCheck.java @@ -1,15 +1,22 @@ package com.delfood.aop; import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * 매장 id가 첫 번째 파라미터로 와야한다. + * 매장 id를 파라미터로 주어야 한다. * 접속한 사장님이 해당 매장의 주인인지 확인한다. * @author yyy99 * */ @Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) public @interface OwnerShopCheck { - + /** + * 해당 변수의 이름. + * @return + */ + String value(); } diff --git a/src/main/java/com/delfood/aop/RiderLoginCheck.java b/src/main/java/com/delfood/aop/RiderLoginCheck.java new file mode 100644 index 0000000..a7e0fb4 --- /dev/null +++ b/src/main/java/com/delfood/aop/RiderLoginCheck.java @@ -0,0 +1,9 @@ +package com.delfood.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +public @interface RiderLoginCheck { + +} diff --git a/src/main/java/com/delfood/controller/CartControllelr.java b/src/main/java/com/delfood/controller/CartControllelr.java index c2010f2..69bdc96 100644 --- a/src/main/java/com/delfood/controller/CartControllelr.java +++ b/src/main/java/com/delfood/controller/CartControllelr.java @@ -1,5 +1,7 @@ package com.delfood.controller; +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; import com.delfood.aop.MemberLoginCheck; import com.delfood.dto.ItemDTO; import com.delfood.service.CartService; @@ -23,31 +25,31 @@ public class CartControllelr { private CartService cartService; @PostMapping("/members/cart/menus") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void addMenu(@RequestBody ItemDTO item, HttpSession session) { cartService.addOrdersItem(item, SessionUtil.getLoginMemberId(session)); } @GetMapping("/members/cart/menus") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public List getCart(HttpSession session) { return cartService.getItems(SessionUtil.getLoginMemberId(session)); } @DeleteMapping("/members/cart/menus") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void clearCart(HttpSession session) { cartService.claer(SessionUtil.getLoginMemberId(session)); } @DeleteMapping("/members/cart/menus/{index}") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void deleteCartMenu(HttpSession session, @PathVariable long index) { cartService.deleteCartMenu(SessionUtil.getLoginMemberId(session), index); } @GetMapping("/members/cart/price") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public CartPriceResponse cartPrice(HttpSession session) { String memberId = SessionUtil.getLoginMemberId(session); return new CartPriceResponse(cartService.getItems(memberId), cartService.allPrice(memberId)); diff --git a/src/main/java/com/delfood/controller/CouponController.java b/src/main/java/com/delfood/controller/CouponController.java new file mode 100644 index 0000000..c36f6c0 --- /dev/null +++ b/src/main/java/com/delfood/controller/CouponController.java @@ -0,0 +1,78 @@ +package com.delfood.controller; + +import com.delfood.dto.CouponDTO; +import com.delfood.service.CouponService; +import lombok.extern.log4j.Log4j2; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +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.RestController; + +@Log4j2 +@RestController +@RequestMapping("/coupons/") +public class CouponController { + + @Autowired + CouponService couponService; + + /** + * 쿠폰을 추가한다. + * @param couponInfo 쿠폰 정보 + * @return + * + * @author jinyoung + */ + @PostMapping + public void addCoupon(@RequestBody CouponDTO couponInfo) { + + if (CouponDTO.hasNullData(couponInfo)) { + log.error("insufficient coupon information! {}", couponInfo.toString()); + throw new NullPointerException("insufficient coupon information! " + couponInfo.toString()); + } + + couponService.addCoupon(couponInfo); + } + + /** + * 쿠폰 이름과 만료일을 수정한다. + * + * @param id 쿠폰 아이디 + * @param name 수정할 쿠폰 이름 + * @param endAt 수정할 만료일 + * + * @author jinyoung + */ + @PatchMapping + public void updateCouponNameAndEndAt(Long id, String name, LocalDateTime endAt) { + couponService.updateCouponNameAndEndAt(id, name, endAt); + } + + /** + * 쿠폰을 삭제한다. + * + * @param id 쿠폰 아이디 + */ + public void deleteCoupon(Long id) { + couponService.deleteCoupon(id); + } + + + /** + * 만료일이 지나지 않은 쿠폰들을 조회한다. + * @return 쿠폰리스트 + * + * @author jinyoung + */ + @GetMapping + public List getAvailableCoupons() { + return couponService.getAvaliableCoupons(); + } + +} diff --git a/src/main/java/com/delfood/controller/CouponIssueController.java b/src/main/java/com/delfood/controller/CouponIssueController.java new file mode 100644 index 0000000..d1dcccb --- /dev/null +++ b/src/main/java/com/delfood/controller/CouponIssueController.java @@ -0,0 +1,53 @@ +package com.delfood.controller; + +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; +import com.delfood.aop.MemberLoginCheck; +import com.delfood.dto.CouponIssueDTO; +import com.delfood.service.CouponIssueService; +import com.delfood.utils.SessionUtil; +import java.util.List; +import javax.servlet.http.HttpSession; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Log4j2 +@RestController +@RequestMapping("/couponIssues/") +public class CouponIssueController { + + @Autowired + private CouponIssueService couponIssueService; + + /** + * 회원에게 쿠폰을 발행한다. + * @param session 현재 사용자 세션 + * @param couponId 쿠폰 아이디 + */ + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + @LoginCheck(type = UserType.MEMBER) + public void addCouponIssue(HttpSession session, @RequestBody Long couponId) { + + couponIssueService.createCouponIssue(SessionUtil.getLoginMemberId(session), couponId); + } + + /** + * 회원이 가지고 있는 발행 쿠폰들을 조회한다. + * @param session 현재 사용자 세션 + * @return + */ + @GetMapping + @LoginCheck(type = UserType.MEMBER) + public List getCouponIssues(HttpSession session) { + return couponIssueService.getCouponIssues(SessionUtil.getLoginMemberId(session)); + } + +} diff --git a/src/main/java/com/delfood/controller/LocationController.java b/src/main/java/com/delfood/controller/LocationController.java index 23c0dcb..d9657bb 100644 --- a/src/main/java/com/delfood/controller/LocationController.java +++ b/src/main/java/com/delfood/controller/LocationController.java @@ -43,7 +43,7 @@ public class LocationController { * @return */ @PostMapping("deliveries/{shopId}/possibles") - @OwnerShopCheck + @OwnerShopCheck("shopId") @ResponseStatus(HttpStatus.CREATED) public void addDeliveryLocation( @PathVariable(name = "shopId") Long shopId, @@ -60,7 +60,7 @@ public void addDeliveryLocation( * @return */ @GetMapping("deliveries/{shopId}/possibles") - @OwnerShopCheck + @OwnerShopCheck("shopId") public List getDeliveryLocations( @PathVariable(name = "shopId") Long shopId) { return shopService.getDeliveryLocations(shopId); @@ -76,7 +76,7 @@ public List getDeliveryLocations( * @return */ @DeleteMapping("deliveries/{shopId}/possibles/{deliveryLocationId}") - @OwnerShopCheck + @OwnerShopCheck("shopId") public void deleteDeliveryLocation( @PathVariable(value = "shopId") Long shopId, @PathVariable(value = "deliveryLocationId") Long deliveryLocationId, diff --git a/src/main/java/com/delfood/controller/MemberController.java b/src/main/java/com/delfood/controller/MemberController.java index eb75318..82935ec 100644 --- a/src/main/java/com/delfood/controller/MemberController.java +++ b/src/main/java/com/delfood/controller/MemberController.java @@ -1,5 +1,7 @@ package com.delfood.controller; +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; import com.delfood.aop.MemberLoginCheck; import com.delfood.dto.MemberDTO; import com.delfood.error.exception.DuplicateIdException; @@ -69,7 +71,7 @@ public class MemberController { * @return MemberDTO */ @GetMapping("myInfo") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public MemberInfoResponse memberInfo(HttpSession session) { String id = SessionUtil.getLoginMemberId(session); MemberDTO memberInfo = memberService.getMemberInfo(id); @@ -149,7 +151,7 @@ public ResponseEntity login(@RequestBody @NonNull MemberLoginRequ * @return 로그인 하지 않았을 시 401코드를 반환하고 result:NO_LOGIN 반환 로그아웃 성공시 200 코드를 반환 */ @GetMapping("logout") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void logout(HttpSession session) { SessionUtil.logoutMember(session); } @@ -162,7 +164,7 @@ public void logout(HttpSession session) { * @return */ @PatchMapping("password") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void updateMemberInfo(HttpSession session, @RequestBody @NotNull UpdateMemberPasswordRequest passwordRequest) { String passwordBeforeChange = passwordRequest.getPasswordBeforeChange(); @@ -183,7 +185,7 @@ public void updateMemberInfo(HttpSession session, * @return */ @DeleteMapping("myInfo") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void deleteMemberInfo(HttpSession session) { String id = SessionUtil.getLoginMemberId(session); memberService.deleteMember(id); @@ -198,7 +200,7 @@ public void deleteMemberInfo(HttpSession session) { * @param session 현재 로그인한 고객의 세션 */ @PatchMapping("address") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public ResponseEntity updateMemberAddress( @RequestBody @NotNull UpdateMemberAddressRequest memberInfo, HttpSession session) { ResponseEntity responseEntity = null; @@ -225,7 +227,7 @@ public ResponseEntity updateMemberAddress( } @PostMapping("token") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public void addToken(HttpSession session, String token) { String memberId = SessionUtil.getLoginMemberId(session); pushService.addMemberToken(memberId, token); diff --git a/src/main/java/com/delfood/controller/OrderController.java b/src/main/java/com/delfood/controller/OrderController.java index d650297..2720bfe 100644 --- a/src/main/java/com/delfood/controller/OrderController.java +++ b/src/main/java/com/delfood/controller/OrderController.java @@ -1,22 +1,32 @@ package com.delfood.controller; +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; import com.delfood.aop.MemberLoginCheck; +import com.delfood.aop.OwnerLoginCheck; import com.delfood.controller.response.OrderResponse; import com.delfood.dto.ItemsBillDTO; import com.delfood.dto.OrderDTO; import com.delfood.dto.OrderItemDTO; +import com.delfood.error.exception.coupon.IssuedCouponExistException; import com.delfood.error.exception.order.TotalPriceMismatchException; import com.delfood.dto.OrderBillDTO; +import com.delfood.service.CouponIssueService; import com.delfood.service.OrderService; +import com.delfood.service.PushService; +import com.delfood.service.ShopService; import com.delfood.utils.SessionUtil; +import java.time.LocalDateTime; import java.util.List; import javax.servlet.http.HttpSession; -import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NonNull; import lombok.extern.log4j.Log4j2; import org.codehaus.commons.nullanalysis.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -31,16 +41,28 @@ public class OrderController { @Autowired OrderService orderService; + @Autowired + ShopService shopService; + + @Autowired + PushService pushService; + + @Autowired + CouponIssueService couponIssueService; + /** * 아이템들의 가격과 정보를 조회한다. + * 쿠폰과 배달 가격을 제외한 순수 아이템 가격만 제공한다. * @author jun * @param items 가격을 계산할 아이템들 * @return */ @GetMapping("price") - @MemberLoginCheck - public long getItemsBill(HttpSession session, @RequestBody List items) { - return orderService.totalPrice(SessionUtil.getLoginMemberId(session), items); + @LoginCheck(type = UserType.MEMBER) + public ItemsBillResponse getItemsBill(HttpSession session, + @RequestBody List items) { + long itemsPrice = orderService.totalPrice(SessionUtil.getLoginMemberId(session), items); + return ItemsBillResponse.builder().itemsPrice(itemsPrice).build(); } /** @@ -50,7 +72,7 @@ public long getItemsBill(HttpSession session, @RequestBody List it * @return */ @GetMapping("{orderId}/bill") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public OrderBillDTO orderInfo(@PathVariable("orderId") Long orderId) { return orderService.getPreOrderBill(orderId); } @@ -63,7 +85,7 @@ public OrderBillDTO orderInfo(@PathVariable("orderId") Long orderId) { * @return */ @PostMapping - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public OrderResponse order(HttpSession session, @RequestBody OrderRequest request) { if (request.getItems().isEmpty()) { // items가 null일때도 NullpointerException이 발생한다 @@ -74,31 +96,49 @@ public OrderResponse order(HttpSession session, @RequestBody OrderRequest reques if (orderService.isShopItems(request.getItems(), request.getShopId()) == false) { log.error("주문하신 매장의 메뉴 또는 옵션이 아닙니다."); throw new IllegalArgumentException("주문하신 매장의 메뉴 또는 옵션이 아닙니다."); - } + } + + // 쿠폰이 유효한지 검증 + if (couponIssueService.isUsed(request.getCouponIssueId())) { + log.info("이미 사용한 쿠폰 사용 시도. 요청 발행 쿠폰 아이디 : {}", request.getCouponIssueId()); + throw new IssuedCouponExistException("이미 사용한 쿠폰입니다"); + } + // 클라이언트가 계산한 금액과 서버에서 계산한 금액이 같은지 비교 - long totalPriceFromServer = - orderService.totalPrice(SessionUtil.getLoginMemberId(session), request.getItems()); - if (totalPriceFromServer != request.getTotalPrice()) { - log.error("Total Price Mismatch! client price : {}, server price : {}", - request.getTotalPrice(), - totalPriceFromServer); + long totalItemsPriceFromServer = orderService.totalPrice(SessionUtil.getLoginMemberId(session), + request.getItems()); + long discountPriceFromServer = + couponIssueService.discountPrice(request.getCouponIssueId(), totalItemsPriceFromServer); + long totalPrice = totalItemsPriceFromServer - discountPriceFromServer; + if (totalPrice != request.getTotalPrice()) { + log.error( + "Total Price Mismatch! client price : {}, server price : {}," + + " totalItemsPriceFromServer : {}, discountPriceFromServer : {}", + request.getTotalPrice(), totalPrice, totalItemsPriceFromServer, discountPriceFromServer); throw new TotalPriceMismatchException("Total Price Mismatch!"); } - return orderService.order(SessionUtil.getLoginMemberId(session), request.getItems(), - request.getShopId()); + OrderResponse orderResponse = orderService.order(SessionUtil.getLoginMemberId(session), + request.getItems(), request.getShopId(), request.getCouponIssueId()); + + return orderResponse; } /** * 아이템 리스트들을 상세하게 계산서로 발행한다. * @param session 사용자의 세션 - * @param items 주문하기 전 아이템들 + * @param billRequest 주문할 아이템들, 쿠폰정보. 쿠폰정보는 Null 가능 * @return */ @GetMapping("bill") - @MemberLoginCheck - public ItemsBillDTO getBill(HttpSession session, @RequestBody List items) { - return orderService.getBill(SessionUtil.getLoginMemberId(session), items); + @LoginCheck(type = UserType.MEMBER) + public ItemsBillDTO getBill(HttpSession session, @RequestBody BillRequest billRequest) { + if (couponIssueService.isUsed(billRequest.getCouponIssueId())) { + log.info("이미 사용한 쿠폰 사용 시도. 요청 발행 쿠폰 아이디 : {}", billRequest.getCouponIssueId()); + throw new IssuedCouponExistException("이미 사용한 쿠폰입니다"); + } + return orderService.getBill(SessionUtil.getLoginMemberId(session), billRequest.getItems(), + billRequest.getCouponIssueId()); } /** @@ -109,7 +149,7 @@ public ItemsBillDTO getBill(HttpSession session, @RequestBody List * @return */ @GetMapping - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public List myOrders(HttpSession session, @Nullable Long lastViewedOrderId) { return orderService.getMemberOrder(SessionUtil.getLoginMemberId(session), lastViewedOrderId); } @@ -122,7 +162,7 @@ public List myOrders(HttpSession session, @Nullable Long lastViewedOrd * @return */ @GetMapping("{orderId}") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public OrderDTO getOrder(HttpSession session, @PathVariable Long orderId) { OrderDTO orderInfo = orderService.getOrder(orderId); if (orderInfo == null) { @@ -137,11 +177,79 @@ public OrderDTO getOrder(HttpSession session, @PathVariable Long orderId) { return orderInfo; } + + + // 여기서 부터는 사장님 관련 컨트롤러입니다. + + /** + * 사장님이 소유한 가게에 요청된 주문들을 조회한다. + * 유효한 주문만 조회된다. + * @author jun + * @return + */ + @GetMapping("owner") + @LoginCheck(type = UserType.OWNER) + public List getRequestedOrders(HttpSession session) { + String ownerId = SessionUtil.getLoginOwnerId(session); + List shopOrders = orderService.getOwnerOrderRequest(ownerId); + return shopOrders; + } + + /** + * 주문을 승인한다. + * @author jun + * @param orderId 주문 아이디 + * @param request 주문 승낙시 입력해야하는 정보 + * @param session 사장님 세션 + */ + @PatchMapping("{orderId}/approve") + @LoginCheck(type = UserType.OWNER) + public void orderApprove(@PathVariable(name = "orderId") Long orderId, + @RequestBody OrderApproveRequest request, + HttpSession session) { + String ownerId = SessionUtil.getLoginOwnerId(session); + + // 해당 주문에 대한 권한이 있는지 확인한다 + if (orderService.isOwnerOrder(ownerId, orderId) == false) { + throw new IllegalArgumentException("해당 주문에 대한 권한이 없습니다."); + } + + // 주문 승인을 진행한다 + orderService.orderApprove(orderId, request.getMinute()); + } + + // request @Getter private static class OrderRequest { + @NonNull private Long shopId; + @NonNull private List items; + @Nullable + private Long couponIssueId; private long totalPrice; } + + @Getter + private static class BillRequest { + private List items; + @Nullable + private Long couponIssueId; + } + + @Getter + private static class OrderApproveRequest { + @NonNull + private Long minute; // 몇분이나 걸릴지 입력한 값 + + } + + // response + + @Builder + @Getter + private static class ItemsBillResponse { + private long itemsPrice; + } } diff --git a/src/main/java/com/delfood/controller/OwnerController.java b/src/main/java/com/delfood/controller/OwnerController.java index efe1ceb..fec58da 100644 --- a/src/main/java/com/delfood/controller/OwnerController.java +++ b/src/main/java/com/delfood/controller/OwnerController.java @@ -1,5 +1,7 @@ package com.delfood.controller; +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; import com.delfood.aop.MemberLoginCheck; import com.delfood.aop.OwnerLoginCheck; import com.delfood.dto.OwnerDTO; @@ -107,7 +109,7 @@ public ResponseEntity login(@RequestBody OwnerLoginRequest l * @return */ @GetMapping("logout") - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public void logout(HttpSession session) { SessionUtil.logoutOwner(session); } @@ -120,7 +122,7 @@ public void logout(HttpSession session) { * @return */ @GetMapping("myInfo") - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public OwnerInfoResponse ownerInfo(HttpSession session) { String id = SessionUtil.getLoginOwnerId(session); OwnerDTO ownerInfo = ownerService.getOwner(id); @@ -135,7 +137,7 @@ public OwnerInfoResponse ownerInfo(HttpSession session) { * @return */ @PatchMapping - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public void updateOwnerInfo( @RequestBody UpdateOwnerMailAndTelRequest updateRequest, HttpSession session) { @@ -159,7 +161,7 @@ public void updateOwnerInfo( * @return */ @PatchMapping("password") - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public void updatePassword( @RequestBody UpdateOwnerPasswordRequest passwordResquest, HttpSession session) { String id = SessionUtil.getLoginOwnerId(session); @@ -173,7 +175,7 @@ public void updatePassword( } @PostMapping("token") - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public void addToken(HttpSession session, String token) { String ownerId = SessionUtil.getLoginOwnerId(session); pushService.addOwnerToken(ownerId, token); diff --git a/src/main/java/com/delfood/controller/RiderController.java b/src/main/java/com/delfood/controller/RiderController.java new file mode 100644 index 0000000..6164206 --- /dev/null +++ b/src/main/java/com/delfood/controller/RiderController.java @@ -0,0 +1,164 @@ +package com.delfood.controller; + +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; +import com.delfood.aop.RiderLoginCheck; +import com.delfood.dto.rider.RiderDTO; +import com.delfood.service.rider.RiderInfoService; +import com.delfood.utils.SessionUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Objects; +import javax.servlet.http.HttpSession; +import lombok.Getter; +import lombok.NonNull; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Log4j2 +@RestController +@RequestMapping("/riders/") +public class RiderController { + + @Autowired + private RiderInfoService riderInfoService; + + @Autowired + private ObjectMapper objectMapper; + + /** + * 아이디 중복 체크. + * @author jun + * @param riderId 중복체크할 아이디 + * @return 중복된 아이디라면 true + */ + @GetMapping("duplicated/id/{riderId}") + public boolean isDuplicatedId(@PathVariable(name = "riderId") String riderId) { + return riderInfoService.isDuplicatedId(riderId); + } + + /** + * 라이더 회원가입. + * @author jun + * @param riderInfo 회원가입할 아이디 정보 + * @throws JsonProcessingException 로그를 기록할 때 직렬화중 생길 수 있는 예외 + */ + @PostMapping("signUp") + @ResponseStatus(code = HttpStatus.CREATED) + public void signUp(@RequestBody RiderDTO riderInfo) throws JsonProcessingException { + if (riderInfo.hasNullData()) { + log.info("회원가입 필수 데이터 누락. 요청 정보 : {}", objectMapper.writeValueAsString(riderInfo)); + throw new NullPointerException("라이더 회원가입에 필수 데이터가 누락되었습니다."); + } + + RiderDTO encryptRiderInfo = RiderDTO.encryptDTO(riderInfo); + riderInfoService.signUp(encryptRiderInfo); + } + + /** + * 라이더 로그인을 진행한다. + * + * @author jun + * @param request id, password 정보 + * @param session 현재 세션 + * @return + */ + @PostMapping("login") + public RiderDTO signIn(@RequestBody SignInRequest request, HttpSession session) { + if (Objects.isNull(SessionUtil.getLoginRiderId(session)) == false) { + logout(session); + } + + RiderDTO riderInfo = riderInfoService.signIn(request.getId(), request.getPassword()); + SessionUtil.setLoginRiderId(session, riderInfo.getId()); + return riderInfo; + } + + + /** + * 라이더 로그아웃을 진행한다. + * @author jun + * @param session 사용자의 세션 + */ + @GetMapping("logout") + public void logout(HttpSession session) { + SessionUtil.logoutRider(session); + } + + /** + * 라이더의 비밀번호를 변경한다. + * @param session 사용자의 세션 + * @param request 변경전 비밀번호, 변경할 비밀번호 정보 + */ + @PatchMapping("password") + @LoginCheck(type = UserType.RIDER) + public void updatePassword(HttpSession session, @RequestBody UpdatePasswordRequest request) { + String id = SessionUtil.getLoginRiderId(session); + riderInfoService.changePassword(id, request.getPasswordBeforechange(), + request.getPasswordAfterChange()); + } + + /** + * 라이더의 계정을 삭제한다. + * 삭제가 완료된다면 로그아웃된다. + * @param session 현제 사용자의 세션 + * @param password 유효성 검사를 위한 계정 비밀번호 + */ + @DeleteMapping + @LoginCheck(type = UserType.RIDER) + public void deleteRiderAccount(HttpSession session, String password) { + String id = SessionUtil.getLoginRiderId(session); + riderInfoService.deleteAccount(id, password); + SessionUtil.logoutRider(session); + } + + @PatchMapping("mail") + public void updateMail(HttpSession session, @RequestBody UpdateMailRequest request) { + String id = SessionUtil.getLoginRiderId(session); + riderInfoService.changeMail(id, request.getPassword(), request.getUpdateMail()); + } + + + + // Request + @Getter + private static class SignInRequest { + @NonNull + private String id; + + @NonNull + private String password; + } + + @Getter + private static class UpdatePasswordRequest { + @NonNull + private String passwordBeforechange; + + @NonNull + private String passwordAfterChange; + } + + @Getter + private static class UpdateMailRequest { + @NonNull + private String password; + + @NonNull + private String updateMail; + } + + + +} diff --git a/src/main/java/com/delfood/controller/ShopController.java b/src/main/java/com/delfood/controller/ShopController.java index a7bd6f1..aad5125 100644 --- a/src/main/java/com/delfood/controller/ShopController.java +++ b/src/main/java/com/delfood/controller/ShopController.java @@ -15,8 +15,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.delfood.aop.LoginCheck; import com.delfood.aop.OwnerLoginCheck; import com.delfood.aop.OwnerShopCheck; +import com.delfood.aop.LoginCheck.UserType; import com.delfood.dto.AddressDTO; import com.delfood.dto.DeliveryLocationDTO; import com.delfood.dto.OwnerDTO; @@ -48,7 +50,7 @@ public class ShopController { * @return */ @PostMapping - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public ResponseEntity addShop(HttpSession session, @RequestBody ShopDTO shopInfo) { String ownerId = SessionUtil.getLoginOwnerId(session); @@ -71,7 +73,7 @@ public ResponseEntity addShop(HttpSession session, * @return 페이지에 따른 사장님 매장, 총 매장 개수 */ @GetMapping - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public MyShopsResponse myShops(MyShopsRequest myShopsRequest, HttpSession session) { String id = SessionUtil.getLoginOwnerId(session); @@ -90,7 +92,7 @@ public MyShopsResponse myShops(MyShopsRequest myShopsRequest, * @return */ @PatchMapping("{id}") - @OwnerShopCheck + @OwnerShopCheck("id") public void updateShop(@PathVariable Long id, @RequestBody(required = true) final ShopUpdateDTO updateInfo, HttpSession session) { final ShopUpdateDTO copyData = ShopUpdateDTO.copyWithId(updateInfo, id); @@ -107,7 +109,7 @@ public void updateShop(@PathVariable Long id, * @return */ @PatchMapping("open/{id}") - @OwnerShopCheck + @OwnerShopCheck("id") public ShopDTO openShop( @PathVariable(value = "id", required = true) Long id, HttpSession session) { shopService.openShop(id); @@ -123,7 +125,7 @@ public ShopDTO openShop( * @return 오픈한 매장의 id, 이름 */ @PatchMapping("open/") - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public List openAllShops(HttpSession session) { String ownerId = SessionUtil.getLoginOwnerId(session); List openShops = shopService.openAllShops(ownerId); @@ -140,7 +142,7 @@ public List openAllShops(HttpSession session) { * @return */ @PatchMapping("close/{id}") - @OwnerShopCheck + @OwnerShopCheck("id") public ShopDTO closeShop( @PathVariable(value = "id", required = true) Long id, HttpSession session) { shopService.closeShop(id); @@ -156,7 +158,7 @@ public ShopDTO closeShop( * @return 운영 종료를 진행한 매장의 id, 이름 */ @PatchMapping("close/") - @OwnerLoginCheck + @LoginCheck(type = UserType.OWNER) public List closeAllShops(HttpSession session) { String ownerId = SessionUtil.getLoginOwnerId(session); return shopService.closeAllShops(ownerId); @@ -170,7 +172,7 @@ public List closeAllShops(HttpSession session) { * @return */ @GetMapping("{shopId}") - @OwnerShopCheck + @OwnerShopCheck("shopId") public ShopInfoResponse shopInfo( @PathVariable(value = "shopId", required = true) Long shopId, HttpSession session) { ShopDTO shopInfo = shopService.getShop(shopId); diff --git a/src/main/java/com/delfood/controller/ShopSearchController.java b/src/main/java/com/delfood/controller/ShopSearchController.java index 64ab6d7..f76ade3 100644 --- a/src/main/java/com/delfood/controller/ShopSearchController.java +++ b/src/main/java/com/delfood/controller/ShopSearchController.java @@ -1,5 +1,7 @@ package com.delfood.controller; +import com.delfood.aop.LoginCheck; +import com.delfood.aop.LoginCheck.UserType; import com.delfood.aop.MemberLoginCheck; import com.delfood.dto.ShopCategoryDTO; import com.delfood.dto.ShopDTO; @@ -52,7 +54,7 @@ public GetShopCategoriesResponse getShopCategories() { * @return */ @GetMapping("/available/shops") - @MemberLoginCheck + @LoginCheck(type = UserType.MEMBER) public GetShopByCategoryIdAndTownCodeResponse getShopsByCategoryIdAndTownCode( @RequestParam(required = true) Long categoryId, HttpSession session) { String memberId = SessionUtil.getLoginMemberId(session); diff --git a/src/main/java/com/delfood/dao/FcmDao.java b/src/main/java/com/delfood/dao/FcmDao.java index 869ddee..3efe483 100644 --- a/src/main/java/com/delfood/dao/FcmDao.java +++ b/src/main/java/com/delfood/dao/FcmDao.java @@ -1,7 +1,9 @@ package com.delfood.dao; +import com.delfood.dto.push.PushMessageForOne; import com.delfood.utils.RedisKeyFactory; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.firebase.messaging.Message; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -108,6 +110,15 @@ public List getOwnerTokens(String ownerId) { .map(e -> objectMapper.convertValue(e, String.class)) .collect(Collectors.toList()); } + + public void addMemberErrorPush(String memberId, List messages) { + redisTemplate.opsForList().rightPush(RedisKeyFactory.generateFcmMemberErrorKey(memberId), + messages); + } + public void addOwnerErrorPush(String ownerId, List messages) { + redisTemplate.opsForList().rightPush(RedisKeyFactory.generateFcmOwnerErrorKey(ownerId), + messages); + } } diff --git a/src/main/java/com/delfood/dto/CouponDTO.java b/src/main/java/com/delfood/dto/CouponDTO.java new file mode 100644 index 0000000..b332dc3 --- /dev/null +++ b/src/main/java/com/delfood/dto/CouponDTO.java @@ -0,0 +1,58 @@ +package com.delfood.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; +import java.time.LocalDateTime; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.apache.ibatis.type.Alias; + +@ToString +@Getter +@Setter +@Alias("coupon") +public class CouponDTO { + + public enum DiscountType { + WON, PERCENT + } + + public enum Status { + DEFAULT, DELETED + } + + private Long id; + + private String name; + + private DiscountType discountType; + + private Long discountValue; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updatedAt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endAt; + + private Status status; + + /** + * 쿠폰 등록에 필요한 내용이 null인지 확인. + * @param couponInfo 쿠폰정보 + * @return + * + * @author jinyoung + */ + public static boolean hasNullData(CouponDTO couponInfo) { + return couponInfo.getName() == null || couponInfo.getDiscountType() == null + || couponInfo.getDiscountValue() == null || couponInfo.getEndAt() == null; + + } +} diff --git a/src/main/java/com/delfood/dto/CouponIssueDTO.java b/src/main/java/com/delfood/dto/CouponIssueDTO.java new file mode 100644 index 0000000..2525dbe --- /dev/null +++ b/src/main/java/com/delfood/dto/CouponIssueDTO.java @@ -0,0 +1,49 @@ +package com.delfood.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.apache.ibatis.type.Alias; + +@Setter +@Getter +@ToString +@Alias("couponIssue") +public class CouponIssueDTO { + + public enum Status { + DEFAULT, USED + } + + public enum DiscountType { + WON, PERCENT + } + + private Long id; + + private String memberId; + + private Long couponId; + + private Status status; + + private Long paymentId; + + private DiscountType discountType; + + private String name; + + private Long discountValue; + + @JsonFormat(pattern = "yy-MM-dd hh:mm:ss") + private LocalDateTime createdAt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endAt; + +} diff --git a/src/main/java/com/delfood/dto/ItemDTO.java b/src/main/java/com/delfood/dto/ItemDTO.java index 416c743..7a87817 100644 --- a/src/main/java/com/delfood/dto/ItemDTO.java +++ b/src/main/java/com/delfood/dto/ItemDTO.java @@ -2,12 +2,15 @@ import java.util.List; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @EqualsAndHashCode(of = {"menuInfo", "options", "shopInfo"}) +@NoArgsConstructor +@AllArgsConstructor public class ItemDTO { private CacheMenuDTO menuInfo; // id, name, price private List options; // id, name, price @@ -30,6 +33,8 @@ public boolean hasNullDataBeforeInsertCart() { @Getter @EqualsAndHashCode + @NoArgsConstructor + @AllArgsConstructor public static class CacheMenuDTO { private Long id; private String name; @@ -38,6 +43,8 @@ public static class CacheMenuDTO { @Getter @EqualsAndHashCode + @NoArgsConstructor + @AllArgsConstructor public static class CacheShopDTO { private Long id; private String name; @@ -45,6 +52,7 @@ public static class CacheShopDTO { @Getter @EqualsAndHashCode + @AllArgsConstructor public static class CacheOptionDTO { private Long id; private String name; diff --git a/src/main/java/com/delfood/dto/ItemsBillDTO.java b/src/main/java/com/delfood/dto/ItemsBillDTO.java index 53af7ce..2856503 100644 --- a/src/main/java/com/delfood/dto/ItemsBillDTO.java +++ b/src/main/java/com/delfood/dto/ItemsBillDTO.java @@ -1,19 +1,29 @@ package com.delfood.dto; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import org.codehaus.jackson.annotate.JsonIgnore; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; +import lombok.extern.log4j.Log4j2; // 사용자에게 전달하는 최종 주문서 DTO @Getter @NoArgsConstructor +@Log4j2 public class ItemsBillDTO { @NonNull private List menus; + private long itemsPrice; + + private long discountPrice; + private long totalPrice; @NonNull private String memberId; @@ -24,6 +34,8 @@ public class ItemsBillDTO { private DeliveryInfo deliveryInfo; + private CouponInfo couponInfo; + /** * 해당 인자를 세팅하여 새로운 객체를 반환한다. * 리스트인 'menus'에는 ArrayList를 할당한다. @@ -36,8 +48,9 @@ public ItemsBillDTO(@NonNull String memberId, @NonNull ShopInfo shopInfo, double distanceMeter, long deliveryPrice, - long itemsPrice, - List menus) { + List menus, + CouponInfo couponInfo, + List ordersItems) { this.memberId = memberId; this.addressInfo = addressInfo; this.shopInfo = shopInfo; @@ -45,7 +58,30 @@ public ItemsBillDTO(@NonNull String memberId, .deliveryPrice(deliveryPrice) .build(); this.menus = menus; - this.totalPrice = totalPrice(); + this.couponInfo = couponInfo; + + // 아이템 개수 맞춰주기 + log.debug("아이템 개수 맞추기 시작"); + ordersItems.stream().forEach(item -> { + menus.stream().forEach(menu -> { + log.debug("비교 메뉴 1 : {}, 비교 메뉴 2 : {}", menu.getId(), item.getMenuId()); + log.debug("비교 옵션 1 : {}, 비교 옵션 2 : {}", + item.getOptions().stream().mapToLong(e -> e.getOptionId()).sorted().toArray(), + menu.getOptions().stream().mapToLong(e -> e.getId()).sorted().toArray()); + log.debug("메뉴 비교 결과 : {}, 옵션 비교 결과 : {}", menu.getId() == item.getMenuId(), + item.getOptions().stream().mapToLong(e -> e.getOptionId()).sorted().toArray() + .equals(menu.getOptions().stream().mapToLong(e -> e.getId()).sorted().toArray())); + if (menu.getId() == item.getMenuId() && Arrays.equals( + item.getOptions().stream().mapToLong(e -> e.getOptionId()).sorted().toArray(), + menu.getOptions().stream().mapToLong(e -> e.getId()).sorted().toArray())) { + log.debug("같은 아이템 발견. 카운트 : {} ", item.getCount()); + menu.count = item.getCount(); + } + }); + }); + log.debug("아이템 개수 맞추기 끝"); + + totalPrice(); } @Getter @@ -55,6 +91,7 @@ public static class MenuInfo { private String name; private long price; private List options; + private long count; /** * 메뉴 정보의 간략한 정보를 저장하는 DTO를 생성한다. @@ -63,10 +100,11 @@ public static class MenuInfo { * @param price 메뉴 가격 */ @Builder - public MenuInfo(long id, @NonNull String name, long price) { + public MenuInfo(long id, @NonNull String name, long price, long count) { this.id = id; this.name = name; this.price = price; + this.count = count; options = new ArrayList(); } @@ -111,15 +149,75 @@ public static class DeliveryInfo { private long deliveryPrice; } + @Getter + @NoArgsConstructor + public static class CouponInfo { + private long couponIssueId; + private long couponId; + private String memberId; + private String name; + private CouponDTO.DiscountType discountType; + private long discountValue; + private LocalDateTime createdAt; + private LocalDateTime endAt; + + /** + * 직접 쿠폰 정보를 생설할 경우 사용하는 빌더. + * @param couponIssueId 발행 쿠폰 아이디 + * @param couponId 쿠폰 아이디 + * @param memberId 회원 아이디 + * @param name 쿠폰 이름 + * @param discountType 할인 타입 + * @param discountValue 할인 가격 + * @param createAt 발행일 + * @param endAt 만료일 + */ + @Builder + public CouponInfo(long couponIssueId, long couponId, String memberId, String name, + CouponDTO.DiscountType discountType, long discountValue, LocalDateTime createAt, LocalDateTime endAt) { + this.couponIssueId = couponIssueId; + this.couponId = couponId; + this.memberId = memberId; + this.name = name; + this.discountType = discountType; + this.discountValue = discountValue; + this.createdAt = createAt; + this.endAt = endAt; + } + } + /** - * 메뉴 가격, 옵션 가격, 배달 가격을 합친 총 가격을 계산한다. + * 메뉴 가격, 옵션 가격, 배달 가격, 할인 가격을 합친 총 가격을 계산한다. + * 해당 인스턴스의 내부 상태를 변경시킨다. * @author jun * @return */ public long totalPrice() { - return menus.stream().mapToLong(menu -> menu.getPrice() - + menu.getOptions().stream().mapToLong(option -> option.getPrice()).sum()).sum() - + deliveryInfo.getDeliveryPrice(); + long itemsPrice = menus.stream().mapToLong(menu -> { + log.debug("메뉴 가격 : {}, 개수 : {}", menu.getPrice(), menu.getCount()); + return (menu.getPrice() + + menu.getOptions().stream().mapToLong(option -> option.getPrice()).sum()) + * menu.getCount(); + }).sum(); + + long couponDiscountPrice; + + if (couponInfo != null) { // 사용하는 쿠폰이 있을 경우 + if (couponInfo.getDiscountType() == CouponDTO.DiscountType.PERCENT) { + // 이렇게하면 소수점 미만이 버림된다. + couponDiscountPrice = itemsPrice * couponInfo.getDiscountValue() / 100L; + } else { + couponDiscountPrice = couponInfo.getDiscountValue(); + } + } else { // 쿠폰을 사용하지 않을 경우 + couponDiscountPrice = 0; + } + + this.itemsPrice = itemsPrice; + this.discountPrice = couponDiscountPrice; + this.totalPrice = itemsPrice + deliveryInfo.getDeliveryPrice() - couponDiscountPrice; + + return totalPrice; } } diff --git a/src/main/java/com/delfood/dto/OrderBillDTO.java b/src/main/java/com/delfood/dto/OrderBillDTO.java index 0783561..9281e07 100644 --- a/src/main/java/com/delfood/dto/OrderBillDTO.java +++ b/src/main/java/com/delfood/dto/OrderBillDTO.java @@ -12,6 +12,7 @@ public class OrderBillDTO { private String memberId; private OrderStatus orderStatus; private LocalDateTime startTime; + private SimpleCouponInfo couponInfo; private Long deliveryCost; private SimpleAddressInfo addressInfo; private List menus; @@ -27,4 +28,5 @@ public static class SimpleAddressInfo { private Integer buildingSideNumber; private String addressDetail; } + } diff --git a/src/main/java/com/delfood/dto/OrderDTO.java b/src/main/java/com/delfood/dto/OrderDTO.java index b3bda8a..f69d64c 100644 --- a/src/main/java/com/delfood/dto/OrderDTO.java +++ b/src/main/java/com/delfood/dto/OrderDTO.java @@ -55,7 +55,10 @@ public enum OrderStatus { @Nullable private String shopName; - List items; + private List items; + + @Nullable + private SimpleCouponInfo couponInfo; @Builder public OrderDTO(String memberId, String addressCode, String addressDetail, Long shopId, diff --git a/src/main/java/com/delfood/dto/ShopDTO.java b/src/main/java/com/delfood/dto/ShopDTO.java index 4787855..22065a9 100644 --- a/src/main/java/com/delfood/dto/ShopDTO.java +++ b/src/main/java/com/delfood/dto/ShopDTO.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; @@ -10,6 +11,7 @@ @Getter @Setter +@Builder @EqualsAndHashCode(of = {"id"}) @ToString public class ShopDTO { diff --git a/src/main/java/com/delfood/dto/SimpleCouponInfo.java b/src/main/java/com/delfood/dto/SimpleCouponInfo.java new file mode 100644 index 0000000..7eb49b8 --- /dev/null +++ b/src/main/java/com/delfood/dto/SimpleCouponInfo.java @@ -0,0 +1,15 @@ +package com.delfood.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SimpleCouponInfo { + private Long couponIssueId; + private Long couponId; + private String couponName; + private CouponDTO.DiscountType discountType; + private Long discountValue; + private Long discountPrice; +} diff --git a/src/main/java/com/delfood/dto/push/PushMessage.java b/src/main/java/com/delfood/dto/push/PushMessage.java index 98a00fc..0fcc1db 100644 --- a/src/main/java/com/delfood/dto/push/PushMessage.java +++ b/src/main/java/com/delfood/dto/push/PushMessage.java @@ -1,5 +1,6 @@ package com.delfood.dto.push; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NonNull; import org.joda.time.LocalDateTime; @@ -11,6 +12,14 @@ public class PushMessage { @NonNull private String message; + public static final PushMessage ADD_ORDER_REQUEST = new PushMessage("DelFood 주문", "새로운 주문이 들어왔습니다"); + public static final PushMessage ACCEPT_ORDER_REQUEST = new PushMessage("DelFood 접수", "주문이 접수되었습니다"); + public static final PushMessage REQUIRED_ORDER_REQUEST = new PushMessage("DelFood 주문취소", "매장에서 주문을 취소하였습니다"); + public static final PushMessage DELIVERY_MATCH = new PushMessage("DelFood 배달원 매칭", "배달원이 매칭되었습니다"); + public static final PushMessage DELIVERY_START = new PushMessage("DelFood 배달 시작", "음식 배달이 시작되었습니다"); + public static final PushMessage DELIVERY_SUCCESS = new PushMessage("DelFood 배달 완료", "배달이 완료되었습니다"); + + private LocalDateTime generatedTime; public PushMessage(String title, String message) { @@ -18,4 +27,7 @@ public PushMessage(String title, String message) { this.message = message; this.generatedTime = LocalDateTime.now(); } + + + } diff --git a/src/main/java/com/delfood/dto/rider/RiderDTO.java b/src/main/java/com/delfood/dto/rider/RiderDTO.java new file mode 100644 index 0000000..3899931 --- /dev/null +++ b/src/main/java/com/delfood/dto/rider/RiderDTO.java @@ -0,0 +1,106 @@ +package com.delfood.dto.rider; + +import java.time.LocalDateTime; +import java.util.Objects; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import org.codehaus.commons.nullanalysis.Nullable; +import org.codehaus.jackson.annotate.JsonIgnore; +import com.delfood.utils.SHA256Util; + +@Getter +@NoArgsConstructor +public class RiderDTO { + @NonNull + private String id; + + @NonNull + private String password; + + @NonNull + private String name; + + @NonNull + private String tel; + + @NonNull + private String mail; + + @Nullable + private Status status = Status.DEFAULT; + + @Nullable + private LocalDateTime createdAt; + + @Nullable + private LocalDateTime updatedAt; + + + public enum Status { + DEFAULT, DELETED + } + + /** + * RiderDTO Class Builder. + * @param id 아이디 + * @param password 비밀번호 + * @param name 이름 + * @param tel 휴대전화 번호 + * @param mail 메일 + * @param status 계정 상태 + * @param createdAt 회원가입일 + * @param updatedAt 회원정보 수정일 + */ + @Builder + public RiderDTO(String id, String password, String name, String tel, String mail, Status status, + LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.password = password; + this.name = name; + this.tel = tel; + this.mail = mail; + this.status = status == null ? Status.DEFAULT : status; + this.createdAt = createdAt == null ? LocalDateTime.now() : createdAt; + this.updatedAt = updatedAt == null ? LocalDateTime.now() : updatedAt; + } + + /** + * null이 허용되지 않는 필드에 null값이 있는지 확인한다. + * @author jun + * @return + */ + public boolean hasNullData() { + return Objects.isNull(this.id) + || Objects.isNull(this.password) + || Objects.isNull(this.name) + || Objects.isNull(this.tel) + || Objects.isNull(this.mail); + } + + /** + * 객체를 복사하여 패스워드를 암호화한 객체를 생성하여 리턴한다. + * @author jun + * @param riderInfo 암호화할 회원 정보 + * @return + */ + public static RiderDTO encryptDTO(RiderDTO riderInfo) { + String encryptPassword = SHA256Util.encryptSHA256(riderInfo.getPassword()); + return RiderDTO.builder() + .id(riderInfo.getId()) + .password(encryptPassword) + .name(riderInfo.getName()) + .tel(riderInfo.getTel()) + .mail(riderInfo.getMail()) + .status(riderInfo.getStatus()) + .createdAt(riderInfo.getCreatedAt()) + .updatedAt(riderInfo.getUpdatedAt()) + .build(); + } + + @JsonIgnore + public String getPassword() { + return this.password; + } +} diff --git a/src/main/java/com/delfood/error/ErrorController.java b/src/main/java/com/delfood/error/ErrorController.java index b4aa6c3..2429380 100644 --- a/src/main/java/com/delfood/error/ErrorController.java +++ b/src/main/java/com/delfood/error/ErrorController.java @@ -1,7 +1,9 @@ package com.delfood.error; import com.delfood.error.exception.DuplicateIdException; +import com.delfood.error.exception.IdDeletedException; import com.delfood.error.exception.cart.DuplicateItemException; +import com.delfood.error.exception.coupon.IssuedCouponExistException; import com.delfood.error.exception.menuGroup.InvalidMenuGroupCountException; import com.delfood.error.exception.menuGroup.InvalidMenuGroupIdException; import com.delfood.error.exception.mockPay.MockPayException; @@ -75,6 +77,12 @@ public ErrorMsg handleCannotShopException(RuntimeException e) { } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IssuedCouponExistException.class) + public ErrorMsg handleIssuedCouponExistException(IssuedCouponExistException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + @ResponseStatus(HttpStatus.CONFLICT) @ExceptionHandler(DuplicateItemException.class) public ErrorMsg handleDuplicatedItemException(DuplicateItemException e) { @@ -86,4 +94,10 @@ public ErrorMsg handleDuplicatedItemException(DuplicateItemException e) { public ErrorMsg handleMockPayException(MockPayException e) { return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); } + + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(IdDeletedException.class) + public ErrorMsg handleIdDeletedException(IdDeletedException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } } diff --git a/src/main/java/com/delfood/error/exception/DuplicateException.java b/src/main/java/com/delfood/error/exception/DuplicateException.java new file mode 100644 index 0000000..902cca2 --- /dev/null +++ b/src/main/java/com/delfood/error/exception/DuplicateException.java @@ -0,0 +1,7 @@ +package com.delfood.error.exception; + +public class DuplicateException extends RuntimeException{ + public DuplicateException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/delfood/error/exception/IdDeletedException.java b/src/main/java/com/delfood/error/exception/IdDeletedException.java new file mode 100644 index 0000000..a34248b --- /dev/null +++ b/src/main/java/com/delfood/error/exception/IdDeletedException.java @@ -0,0 +1,7 @@ +package com.delfood.error.exception; + +public class IdDeletedException extends IllegalArgumentException { + public IdDeletedException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/delfood/error/exception/coupon/IssuedCouponExistException.java b/src/main/java/com/delfood/error/exception/coupon/IssuedCouponExistException.java new file mode 100644 index 0000000..7f3d0d4 --- /dev/null +++ b/src/main/java/com/delfood/error/exception/coupon/IssuedCouponExistException.java @@ -0,0 +1,7 @@ +package com.delfood.error.exception.coupon; + +public class IssuedCouponExistException extends RuntimeException{ + public IssuedCouponExistException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/delfood/mapper/CouponIssueMapper.java b/src/main/java/com/delfood/mapper/CouponIssueMapper.java new file mode 100644 index 0000000..db19ae6 --- /dev/null +++ b/src/main/java/com/delfood/mapper/CouponIssueMapper.java @@ -0,0 +1,61 @@ +package com.delfood.mapper; + +import java.util.List; +import org.springframework.stereotype.Repository; +import com.delfood.dto.CouponIssueDTO; +import com.delfood.dto.ItemsBillDTO.CouponInfo; + +@Repository +public interface CouponIssueMapper { + + /** + * 해당 쿠폰아이디로 발급된 적이 있는 쿠폰의 수를 조회한다. + * + * @param couponId 쿠폰 아이디 + * @return 발급된 쿠폰의 수 + * + * @author jinyoung + */ + public int countCouponIssue(Long couponId); + + /** + * 회원 아이디와 쿠폰 아이디를 통해 이미 발급된 쿠폰의 수를 조회한다. + * + * @param memberId 회원 아이디 + * @param couponId 쿠폰 아이디 + * @return 발급된 쿠폰의 수 + * + * @author jinyoung + */ + public int countCouponIssueByMemberIdAndCouponId(String memberId, Long couponId); + + /** + * 발급 쿠폰을 추가한다. + * @param memberId 회원 아이디 + * @param couponId 쿠폰 아이디 + * @return + * + * @author jinyoung + */ + public int insertCouponIssue(String memberId, Long couponId); + + /** + * 발급 쿠폰의 상태를 USED로 변경한다. + * @param id 발급 쿠폰 아이디 + * @param paymentId 결제 아이디 + * @author jinyoung + */ + public int updateCouponIssueStatusToUsed(Long id, Long paymentId); + + /** + * 회원이 가진 쿠폰들을 조회한다. + * @param memberId 회원 아이디 + * @return + */ + public List findByMemberId(String memberId); + + public CouponInfo findInfoById(long couponIssueId); + + public CouponIssueDTO findById(long couponIssueId); + +} diff --git a/src/main/java/com/delfood/mapper/CouponMapper.java b/src/main/java/com/delfood/mapper/CouponMapper.java new file mode 100644 index 0000000..6f99320 --- /dev/null +++ b/src/main/java/com/delfood/mapper/CouponMapper.java @@ -0,0 +1,39 @@ +package com.delfood.mapper; + +import com.delfood.dto.CouponDTO; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.stereotype.Repository; + +@Repository +public interface CouponMapper { + + /** + * 쿠폰 추가. + * @param couponInfo 쿠폰 정보 + * @return + */ + public Long insertCoupon(CouponDTO couponInfo); + + /** + * 쿠폰 이름과 만료일 수정. + * @param id 쿠폰 아이디 + * @param name 이름 + * @param endAt 만료일 + * @return + */ + public int updateCouponNameAndEndAt(Long id, String name, LocalDateTime endAt); + + /** + * 쿠폰 삭제. + * @param id 쿠폰 아이디 + * @return + */ + public int deleteCoupon(Long id); + + /** + * 만료일이 지나지 않은 쿠폰 조회. + * @return + */ + public List findByEndAtGreaterThanNow(); +} diff --git a/src/main/java/com/delfood/mapper/OrderMapper.java b/src/main/java/com/delfood/mapper/OrderMapper.java index 5efe3f4..61721a9 100644 --- a/src/main/java/com/delfood/mapper/OrderMapper.java +++ b/src/main/java/com/delfood/mapper/OrderMapper.java @@ -1,10 +1,12 @@ package com.delfood.mapper; import com.delfood.dto.OrderDTO; +import com.delfood.dto.OrderDTO.OrderStatus; import com.delfood.dto.OrderItemDTO; import com.delfood.dto.OrderItemOptionDTO; import com.delfood.dto.ItemsBillDTO.MenuInfo; import com.delfood.dto.OrderBillDTO; +import java.time.LocalDateTime; import java.util.List; import lombok.NonNull; @@ -29,4 +31,14 @@ public interface OrderMapper { List findByMemberId(String memberId, Long lastViewedOrderId); boolean isShopItem(List items, Long shopId); + + void updateStatus(@NonNull Long orderId, OrderStatus status); + + List findRequestByOwnerId(String shopId); + + String findOwnerIdByOrderId(Long orderId); + + void updateOrderStatusAndExArrivalTime(Long orderId, LocalDateTime exArrivalTime); + + String findMemberIdByOrderId(Long orderId); } diff --git a/src/main/java/com/delfood/mapper/RiderInfoMapper.java b/src/main/java/com/delfood/mapper/RiderInfoMapper.java new file mode 100644 index 0000000..8df7aea --- /dev/null +++ b/src/main/java/com/delfood/mapper/RiderInfoMapper.java @@ -0,0 +1,24 @@ +package com.delfood.mapper; + +import com.delfood.dto.rider.RiderDTO; +import lombok.NonNull; +import org.springframework.stereotype.Repository; + +@Repository +public interface RiderInfoMapper { + + public boolean isExistById(@NonNull String id); + + public void insertRider(@NonNull RiderDTO riderInfo); + + public RiderDTO findByIdAndPassword(@NonNull String id, @NonNull String password); + + public long updatePassword(@NonNull String id, @NonNull String password); + + public long updateStatusAsDeleted(@NonNull String id); + + public boolean isExistAndEffectiveByIdAndPassword(@NonNull String id, + @NonNull String password); + + public long updateMail(@NonNull String id, @NonNull String mail); +} diff --git a/src/main/java/com/delfood/service/CouponIssueService.java b/src/main/java/com/delfood/service/CouponIssueService.java new file mode 100644 index 0000000..5dfa4c9 --- /dev/null +++ b/src/main/java/com/delfood/service/CouponIssueService.java @@ -0,0 +1,147 @@ +package com.delfood.service; + +import java.util.List; +import java.util.Objects; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.delfood.dto.CouponDTO; +import com.delfood.dto.CouponIssueDTO; +import com.delfood.dto.ItemsBillDTO.CouponInfo; +import com.delfood.error.exception.DuplicateException; +import com.delfood.mapper.CouponIssueMapper; +import lombok.extern.log4j.Log4j2; + +@Service +@Log4j2 +public class CouponIssueService { + + @Autowired + private CouponIssueMapper couponIssueMapper; + + @Autowired + private CouponService couponService; + + /** + * 쿠폰이 발급된 적이 있는 지 조회한다. + * @param couponId 쿠폰 아이디 + * @return 쿠폰의 발급여부 ( true : 발급된 적 있음 , false : 발급된 적 없음 + * + * @author jinyoung + */ + public boolean isIssued(Long couponId) { + return couponIssueMapper.countCouponIssue(couponId) > 0; + } + + /** + * 회원이 이미 해당 쿠폰을 발급받은 적이 있는지 체크한다. + * @param memberId 회원 아이디 + * @param couponId 쿠폰 아이디 + * @return + */ + public boolean checkDuplicateIssue(String memberId, Long couponId) { + return couponIssueMapper.countCouponIssueByMemberIdAndCouponId(memberId, couponId) > 0; + } + + /** + * 회원에게 쿠폰을 발급한다. + * @param memberId 회원 아이디 + * @param couponId 쿠폰 아이디 + * + * @author jinyoung + */ + @Transactional(rollbackFor = RuntimeException.class) + public void createCouponIssue(String memberId, Long couponId) { + + if (checkDuplicateIssue(memberId, couponId)) { + log.error("coupon has already been issued! memberid :{}, couponId :{}", memberId, couponId); + throw new DuplicateException("coupon has already been issued!"); + } + + int result = couponIssueMapper.insertCouponIssue(memberId, couponId); + if (result != 1) { + log.error("insert couponIssue Error! memberId : {}, couponId : {}", memberId, couponId); + throw new RuntimeException("insert couponIssue Error! "); + } + } + + /** + * 발급 쿠폰 사용. + * 발급 쿠폰의 상태를 사용됨으로 변경한다. + * @param id 발급 쿠폰 아이디 + * + * @author jinyoung + */ + @Transactional(rollbackFor = RuntimeException.class) + public void useCouponIssue(Long id, Long paymentId) { + int result = couponIssueMapper.updateCouponIssueStatusToUsed(id, paymentId); + if (result != 1) { + log.error("update coupon status error! id : {}", id); + throw new RuntimeException("update coupon status error!"); + } + } + + /** + * 회원이 가진 발행 쿠폰들을 조회한다. + * @param memberId 회원 아이디 + * @return + */ + public List getCouponIssues(String memberId) { + return couponIssueMapper.findByMemberId(memberId); + } + + /** + * 발행 쿠폰 아이디를 기준으로 쿠폰 전반 정보를 조회한다. + * @author jun + * @param couponIssueId 발행 쿠폰 아이디 + * @return + */ + public CouponInfo getCouponInfoByIssueId(long couponIssueId) { + return couponIssueMapper.findInfoById(couponIssueId); + } + + + /** + * 쿠폰으로 인한 할인 가격을 계산한다. + * 쿠폰이 퍼센트 쿠폰일 시 입력된 가격을 기준으로 퍼센트 할인 가격을 리턴한다. + * 쿠폰이 정액 할인 쿠폰일 시 쿠폰의 할인값을 리턴한다. + * 쿠폰의 할인 값이 아이템 가격보다 클 시 아이템의 가격을 할인값으로 리턴한다. + * + * @author jun + * @param couponIssueId + * @param price + * @return + */ + public long discountPrice(long couponIssueId, long price) { + CouponInfo couponInfo = getCouponInfoByIssueId(couponIssueId); + long discountPrice = 0; + + if (couponInfo.getDiscountType() == CouponDTO.DiscountType.PERCENT) { + discountPrice = price * couponInfo.getDiscountValue() / 100L; + } else { + discountPrice = couponInfo.getDiscountValue(); + } + + return discountPrice > price ? price : discountPrice; + } + + /** + * 해당 발행 쿠폰이 사용상태인지 확인한다. 사용한 쿠폰이라면 true를 반환한다. + * @author jun + * @param couponIssueId 발행 쿠폰 아이디 + * @return + */ + public boolean isUsed(long couponIssueId) { + CouponIssueDTO couponIssueInfo = couponIssueMapper.findById(couponIssueId); + + if (Objects.isNull(couponIssueInfo)) { + log.error("발행쿠폰 사용여부 체크 오류! 조회한 발행 쿠폰 정보가 없습니다. 발행 쿠폰 아이디 : {}", couponIssueId); + throw new IllegalArgumentException("잘못된 쿠폰 발행 번호입니다."); + } + + return couponIssueInfo.getStatus().equals(CouponIssueDTO.Status.USED); + } + + + +} diff --git a/src/main/java/com/delfood/service/CouponService.java b/src/main/java/com/delfood/service/CouponService.java new file mode 100644 index 0000000..fc7e8b5 --- /dev/null +++ b/src/main/java/com/delfood/service/CouponService.java @@ -0,0 +1,119 @@ +package com.delfood.service; + +import com.delfood.dto.CouponDTO; +import com.delfood.dto.CouponDTO.DiscountType; +import com.delfood.error.exception.coupon.IssuedCouponExistException; +import com.delfood.mapper.CouponMapper; +import java.time.LocalDateTime; +import java.util.List; +import lombok.extern.log4j.Log4j2; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Log4j2 +public class CouponService { + + @Autowired + private CouponMapper couponMapper; + + @Autowired + private CouponIssueService couponIssueService; + + /** + * 쿠폰 추가. + * @param couponInfo + * + * @author jinyoung + */ + @Transactional(rollbackFor = RuntimeException.class) + public void addCoupon(CouponDTO couponInfo) { + + verifyDiscountData(couponInfo.getDiscountType(), couponInfo.getDiscountValue()); + + Long insertResult = couponMapper.insertCoupon(couponInfo); + + if (insertResult != 1) { + log.error("coupon Insert Error! {}", couponInfo.toString()); + throw new RuntimeException("coupon Insert Error"); + } + + if (couponInfo.getEndAt().isBefore(couponInfo.getCreatedAt())) { + log.error("coupon expiration date is ealire than creation date! " + + "EndAt : {}, startAt : {}",couponInfo.getEndAt(), couponInfo.getCreatedAt()); + throw new IllegalStateException("coupon expiration date is ealire than creation date!"); + } + + } + + /** + * 쿠폰 할인 값 검증. + * @param discountType 할인 타입 + * @param discountValue 할인 값 + * @throws IllegalArgumentException + * + * @author jinyoung + */ + public static void verifyDiscountData(DiscountType discountType, Long discountValue) { + if (DiscountType.PERCENT == discountType + && ((discountValue < 0 || discountValue > 100))) { + log.error("coupon discount setting error! couponType : {} , discountValue : {}", + discountType, discountValue); + throw new IllegalArgumentException("coupon discount setting error!"); + } + } + + /** + * 쿠폰 이름과 만료일 수정. + * + * @param id 쿠폰 아이디 + * @param name 이름 + * @param endAt 만료일 + * + * @author jinyoung + */ + @Transactional(rollbackFor = RuntimeException.class) + public void updateCouponNameAndEndAt(Long id, String name, LocalDateTime endAt) { + if (couponIssueService.isIssued(id)) { + log.error("Issued Coupon already exists"); + throw new IssuedCouponExistException("Issued Coupon already exists"); + } + + int result = couponMapper.updateCouponNameAndEndAt(id, name, endAt); + if (result != 1) { + log.error("coupon update error! id : {}, name : {}, EndAt : {} ", id, name, endAt); + throw new RuntimeException("coupon update error!"); + } + } + + /** + * 쿠폰삭제. + * @param id 쿠폰 아이디 + * + * @author jinyoung + */ + @Transactional(rollbackFor = RuntimeException.class) + public void deleteCoupon(Long id) { + if (couponIssueService.isIssued(id)) { + log.error("Issued Coupon already exists"); + throw new IssuedCouponExistException("Issued Coupon already exists"); + } + + int result = couponMapper.deleteCoupon(id); + if (result != 1) { + log.error("coupon delete error! id : {}",id); + throw new RuntimeException("coupon delete error!"); + } + } + + /** + * 사용 가능한 쿠폰을 조회한다. (만료일이 현재시간 이후의 쿠폰만 조회) + * @return 쿠폰 리스트 + */ + public List getAvaliableCoupons() { + return couponMapper.findByEndAtGreaterThanNow(); + } + +} diff --git a/src/main/java/com/delfood/service/OrderService.java b/src/main/java/com/delfood/service/OrderService.java index 932a8f3..b6184c0 100644 --- a/src/main/java/com/delfood/service/OrderService.java +++ b/src/main/java/com/delfood/service/OrderService.java @@ -3,30 +3,27 @@ import com.delfood.controller.response.OrderResponse; import com.delfood.dto.AddressDTO; import com.delfood.dto.ItemsBillDTO; -import com.delfood.dto.ItemsBillDTO.MenuInfo; import com.delfood.dto.ItemsBillDTO.ShopInfo; -import com.delfood.dto.ItemsBillDTO.MenuInfo.OptionInfo; -import com.delfood.dto.MemberDTO.Status; -import com.delfood.error.exception.order.TotalPriceMismatchException; import com.delfood.dto.MemberDTO; -import com.delfood.dto.MenuDTO; -import com.delfood.dto.OptionDTO; +import com.delfood.dto.OrderBillDTO; import com.delfood.dto.OrderDTO; import com.delfood.dto.OrderItemDTO; import com.delfood.dto.OrderItemOptionDTO; import com.delfood.dto.PaymentDTO; import com.delfood.dto.PaymentDTO.Type; -import com.delfood.dto.OrderBillDTO; -import com.delfood.mapper.OptionMapper; +import com.delfood.dto.push.PushMessage; import com.delfood.mapper.OrderMapper; import com.delfood.utils.OrderUtil; -import lombok.NonNull; -import lombok.extern.log4j.Log4j2; +import com.google.firebase.database.annotations.Nullable; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Objects; +import lombok.NonNull; +import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service @@ -50,8 +47,13 @@ public class OrderService { @Autowired private PaymentService paymentService; + @Autowired + private PushService pushService; + + @Autowired + private CouponIssueService couponIssueService; + /** - * 미완성 로직
* 주문 요청을 진행한다. * 사용자가 주문 요청시 전달받은 가격과, 서버에서 직접 비교한 가격을 비교하여 다르면 예외처리 할 예정. * @param memberId 고객 아이디 @@ -59,13 +61,15 @@ public class OrderService { * @return */ @Transactional - public OrderResponse order(String memberId, List items, long shopId) { - + public OrderResponse order(String memberId, List items, long shopId, + @Nullable Long couponIssueId) { + // 주문 준비 작업. 결제 전. - Long orderId = preOrder(memberId, items, shopId); + Long orderId = doOrder(memberId, items, shopId); // 계산서 발행 - ItemsBillDTO bill = getBill(memberId, items); + ItemsBillDTO bill = getBill(memberId, items, couponIssueId); + // 가상 결제 진행 PaymentDTO paymentInfo = PaymentDTO.builder() @@ -78,8 +82,19 @@ public OrderResponse order(String memberId, List items, long shopI PaymentDTO payResult = mockPayService.pay(paymentInfo); paymentService.insertPayment(payResult); - // 사장님에게 알림(푸시) + // 결제 완료 처리 + updateStatus(orderId, OrderDTO.OrderStatus.ORDER_REQUEST); + + // 쿠폰 사용처리 + if (bill.getCouponInfo() != null) { + couponIssueService.useCouponIssue(bill.getCouponInfo().getCouponIssueId(), payResult.getId()); + } + + // 사장님에게 알림(푸시) + PushMessage pushMsg = PushMessage.ADD_ORDER_REQUEST; + String ownerId = shopService.getShop(shopId).getOwnerId(); + pushService.sendMessageToOwner(pushMsg, ownerId); // Exception이 발생하지 않는다. return new OrderResponse(bill, orderId); } @@ -87,13 +102,14 @@ public OrderResponse order(String memberId, List items, long shopI /** * 주문 테이블에 insert를 진행한다. * 주문 메뉴, 주문 옵션이 추가된다. + * 주문도중 에러가 나더라도 주문기록을 남기기 위해 독자적인 트랜잭션을 가진다. * * @param memberId 고객 아이디 * @param items 주문할 아이템들 * @return */ - @Transactional - private Long preOrder(String memberId, List items, Long shopId) { + @Transactional(propagation = Propagation.NESTED) + private Long doOrder(String memberId, List items, Long shopId) { MemberDTO memberInfo = memberService.getMemberInfo(memberId); OrderDTO order = OrderDTO .builder() @@ -139,11 +155,18 @@ private Long preOrder(String memberId, List items, Long shopId) { * @return */ @Transactional(readOnly = true) - public ItemsBillDTO getBill(String memberId, List items) { + public ItemsBillDTO getBill(String memberId, List items, Long couponIssueId) { // 고객 주소 정보 추출 AddressDTO addressInfo = memberService.getMemberInfo(memberId).getAddressInfo(); // 매장 정보 추출 ShopInfo shopInfo = shopService.getShopByMenuId(items.get(0).getMenuId()); + + // 쿠폰 정보 추출 + ItemsBillDTO.CouponInfo couponInfo = null; + if (couponIssueId != null) { + couponInfo = couponIssueService.getCouponInfoByIssueId(couponIssueId); + } + // 배달료 계산 long deliveryPrice = addressService.deliveryPrice(memberId, shopInfo.getId()); @@ -154,13 +177,15 @@ public ItemsBillDTO getBill(String memberId, List items) { .shopInfo(shopInfo) .deliveryPrice(deliveryPrice) .menus(orderMapper.findItemsBill(items)) + .couponInfo(couponInfo) + .ordersItems(items) .build(); return bill; } /** - * 총 가격을 계산한다. + * 아이템들의 총 가격을 계산한다. * @author jun * @param items 계산할 아이템들 * @return 총 가격 @@ -168,10 +193,7 @@ public ItemsBillDTO getBill(String memberId, List items) { @Transactional(readOnly = true) public long totalPrice(String memberId, List items) { long totalPrice = orderMapper.findItemsPrice(items); - long deliveryPrice = addressService.deliveryPrice(memberId, - shopService.getShopByMenuId(items.get(0).getMenuId()).getId()); - - return totalPrice + deliveryPrice; + return totalPrice; } @@ -209,7 +231,6 @@ public List getMemberOrder(String memberId, Long lastViewedOrderId) { * 주문 번호를 기반으로 주문 상세를 조회한다. * @author jun * @param orderId 주문 아이디 - * @param memberId 고객 아이디 * @return */ public OrderDTO getOrder(Long orderId) { @@ -227,5 +248,49 @@ public OrderDTO getOrder(Long orderId) { public boolean isShopItems(List items, Long shopId) { return orderMapper.isShopItem(items, shopId); } + + /** + * 주문 상태를 변경시킨다. + * @author jun + * @param orderId 주문 아이디 + * @param status 변경시킬 주문 상태 + */ + public void updateStatus(@NonNull Long orderId, OrderDTO.OrderStatus status) { + orderMapper.updateStatus(orderId, status); + } + + /** + * 사장님 아이디를 기반으로 주문 정보를 조회한다. + * @param ownerId 사장님 아이디 + * @return + */ + public List getOwnerOrderRequest(String ownerId) { + return orderMapper.findRequestByOwnerId(ownerId); + } + + public boolean isOwnerOrder(String ownerId, Long orderId) { + String ownerIdByOrderId = orderMapper.findOwnerIdByOrderId(orderId); + return Objects.equals(ownerId, ownerIdByOrderId); + } + + /** + * 해당 주문을 승인하고 도착 예정시간을 설정한다. + * 승인 완료 후 고객에게 푸시 메세지를 전송한다. + * @author jun + * @param orderId 주문 아이디 + * @param minute 배달까지 몇 분 걸릴지 예상시간 + */ + @Transactional + public void orderApprove(Long orderId, long minute) { + + LocalDateTime exArrivalTime = LocalDateTime.now().plusMinutes(minute); + orderMapper.updateOrderStatusAndExArrivalTime(orderId, exArrivalTime); + String memberId = orderMapper.findMemberIdByOrderId(orderId); + + // 푸시메세지 전송 + PushMessage messageInfo = new PushMessage("DelFood 주문 승인", + "사장님이 주문을 승인했어요! 도착 예정 시간 " + minute + "분 후"); + pushService.sendMessageToMember(messageInfo, memberId); + } } diff --git a/src/main/java/com/delfood/service/OwnerService.java b/src/main/java/com/delfood/service/OwnerService.java index 79bbf20..265f477 100644 --- a/src/main/java/com/delfood/service/OwnerService.java +++ b/src/main/java/com/delfood/service/OwnerService.java @@ -1,17 +1,15 @@ package com.delfood.service; import com.delfood.dto.OwnerDTO; +import com.delfood.error.exception.DuplicateException; import com.delfood.error.exception.DuplicateIdException; import com.delfood.mapper.OwnerMapper; import com.delfood.utils.SHA256Util; import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.HttpStatusCodeException; @Service @@ -86,8 +84,9 @@ public OwnerDTO getOwner(String id) { @Transactional(rollbackFor = RuntimeException.class) public void updateOwnerMailAndTel(String id, String password, String mail, String tel) { // 정보 변경시 패스워드를 입력받는다. 해당 패스워드가 틀릴 시 정보는 변경되지 않는다. - if (ownerMapper.findByIdAndPassword(id, password) == null) { - throw new IllegalArgumentException("패스워드가 일치하지 않습니다"); + if (ownerMapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(password)) == null) { + log.error("password does not match"); + throw new IllegalArgumentException("password does not match"); } int result = ownerMapper.updateMailAndTel(id, mail, tel); @@ -101,20 +100,26 @@ public void updateOwnerMailAndTel(String id, String password, String mail, Strin * 사장 비밀번호 수정. * * @param id 아이디 - * @param passwordAfterChange 변경할 비밀번호 + * @param beforePassword 변경전 비밀번호 + * @param afterPassword 변경할 비밀번호 * @return */ @Transactional(rollbackFor = RuntimeException.class) // runtimeException이 발생하면 rollback을 수행한다. - public void updateOwnerPassword(String id, String passwordBeforeChange, String passwordAfterChange) { - if (ownerMapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(passwordBeforeChange)) == null) { // 아이디와 비밀번호 불일치 - throw new IllegalArgumentException(); - } else if (passwordBeforeChange.equals(SHA256Util.encryptSHA256(passwordAfterChange))) { // 이전 패스워드와 동일한 경우 - throw new HttpStatusCodeException(HttpStatus.CONFLICT, "변경 전 패스워드와 중복됩니다") {}; + public void updateOwnerPassword(String id, String beforePassword, String afterPassword) { + + if (ownerMapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(beforePassword)) + == null) { + log.error("id and password do not match id : {}, password : {}",id,beforePassword); + throw new IllegalArgumentException("id and password do not match"); + } else if (StringUtils.equals(beforePassword, afterPassword)) { + log.error("password duplication before: {}, after : {}", + beforePassword, afterPassword); + throw new DuplicateException("password duplication"); } - String cryptoPassword = SHA256Util.encryptSHA256(passwordAfterChange); + String cryptoPassword = SHA256Util.encryptSHA256(afterPassword); int result = ownerMapper.updatePassword(id, cryptoPassword); if (result != 1) { - log.error("updateOwnerPassword ERROR! id : {}, password : {}", id, passwordAfterChange); + log.error("updateOwnerPassword ERROR! id : {}, password : {}", id, afterPassword); throw new RuntimeException("password update error"); } diff --git a/src/main/java/com/delfood/service/PushService.java b/src/main/java/com/delfood/service/PushService.java index fa17efe..0c169a8 100644 --- a/src/main/java/com/delfood/service/PushService.java +++ b/src/main/java/com/delfood/service/PushService.java @@ -71,6 +71,7 @@ public void init() { * @author jun * @param messageInfo 전송할 푸시 정보 */ + @Async("asyncTask") public void sendByToken(PushMessageForOne messageInfo) { Message message = Message.builder() .setToken(messageInfo.getToken()) @@ -84,7 +85,7 @@ public void sendByToken(PushMessageForOne messageInfo) { response = FirebaseMessaging.getInstance().send(message); log.info("Sent message: " + response); } catch (FirebaseMessagingException e) { - throw new RuntimeException(e.getMessage()); + log.error("cannot send message by token. error info : {}", e.getMessage()); } } @@ -106,7 +107,7 @@ public void sendByTopic(PushMessageForTopic topicMessageInfo) { response = FirebaseMessaging.getInstance().send(message); log.info("Sent message: " + response); } catch (FirebaseMessagingException e) { - throw new RuntimeException(e.getMessage()); + log.error("cannot send message by topic. error info : {}", e.getMessage()); } } @@ -118,6 +119,12 @@ public void sendByTopic(PushMessageForTopic topicMessageInfo) { @Async("asyncTask") public void sendMessageToMember(PushMessage messageInfo, String memberId) { List tokens = fcmDao.getMemberTokens(memberId); + + if (tokens.size() == 0) { // 토큰 개수가 0개이면 오류가 발생한다. + log.debug("해당 회원의 FCM 토큰이 없습니다. 회원 아이디 : {}, 메세지 정보 : {}", memberId, messageInfo); + return; + } + List messages = tokens.stream().map(token -> Message.builder() .putData("title", messageInfo.getTitle()) .putData("message", messageInfo.getMessage()) @@ -130,7 +137,8 @@ public void sendMessageToMember(PushMessage messageInfo, String memberId) { response = FirebaseMessaging.getInstance().sendAll(messages); log.info("Sent message: " + response); } catch (FirebaseMessagingException e) { - throw new RuntimeException(e.getMessage()); + log.error("cannot send to member push message. error info : {}", e.getMessage()); + addErrorMemberPush(memberId, messages); } } @@ -142,6 +150,12 @@ public void sendMessageToMember(PushMessage messageInfo, String memberId) { @Async("asyncTask") public void sendMessageToOwner(PushMessage messageInfo, String ownerId) { List tokens = fcmDao.getOwnerTokens(ownerId); + + if (tokens.size() == 0) { + log.debug("해당 사장님의 FCM 토큰이 없습니다. 회원 아이디 : {}, 메세지 정보 : {}", ownerId, messageInfo); + return; + } + List messages = tokens.stream().map(token -> Message.builder() .putData("title", messageInfo.getTitle()) .putData("message", messageInfo.getMessage()) @@ -154,7 +168,8 @@ public void sendMessageToOwner(PushMessage messageInfo, String ownerId) { response = FirebaseMessaging.getInstance().sendAll(messages); log.info("Sent message: " + response); } catch (FirebaseMessagingException e) { - throw new RuntimeException(e.getMessage()); + log.error("cannot send message to owner. error info : {}", e.getMessage()); + addErrorOwnerPush(ownerId, messages); } } @@ -199,4 +214,12 @@ public List getMemberTokens(String memberId) { public List getOwnerTokens(String ownerId) { return fcmDao.getOwnerTokens(ownerId); } + + public void addErrorMemberPush(String memberId, List messages) { + fcmDao.addMemberErrorPush(memberId, messages); + } + + public void addErrorOwnerPush(String ownerId, List messages) { + fcmDao.addMemberErrorPush(ownerId, messages); + } } diff --git a/src/main/java/com/delfood/service/rider/RiderInfoService.java b/src/main/java/com/delfood/service/rider/RiderInfoService.java new file mode 100644 index 0000000..7478520 --- /dev/null +++ b/src/main/java/com/delfood/service/rider/RiderInfoService.java @@ -0,0 +1,147 @@ +package com.delfood.service.rider; + +import com.delfood.dto.rider.RiderDTO; +import com.delfood.error.exception.DuplicateException; +import com.delfood.error.exception.IdDeletedException; +import com.delfood.mapper.RiderInfoMapper; +import com.delfood.utils.SHA256Util; +import java.util.Objects; +import lombok.NonNull; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Log4j2 +public class RiderInfoService { + + @Autowired + private RiderInfoMapper riderInfoMapper; + + private static final IllegalArgumentException passwordMismatchException = + new IllegalArgumentException("비밀번호가 일치하지 않습니다."); + + /** + * 해당 아이디가 중복된 아이디인지 확인한다. + * + * @author jun + * @param riderId 중복인지 검사할 아이디 + * @return + */ + public boolean isDuplicatedId(@NonNull String riderId) { + return riderInfoMapper.isExistById(riderId); + } + + /** + * 라이더 회원가입을 진행한다. + * @param riderInfo 회원 가입 정보 + */ + @Transactional + public void signUp(@NonNull RiderDTO riderInfo) { + if (isDuplicatedId(riderInfo.getId())) { + throw new DuplicateException("아이디 \"" + riderInfo.getId() + "\" 는 이미 가입한 아이디입니다."); + } + + riderInfoMapper.insertRider(riderInfo); + } + + /** + * 라이더 로그인을 진행한다. + * @param id 아이디 + * @param password 암호화 전 비밀번호 + * @return + */ + public RiderDTO signIn(@NonNull String id, @NonNull String password) { + String encryptedPassword = SHA256Util.encryptSHA256(password); + RiderDTO riderInfo = getRiderInfo(id, encryptedPassword); + + if (RiderDTO.Status.DELETED.equals(riderInfo.getStatus())) { + log.info("signIn - 삭제 회원 로그인 시도. id : {}, password : {}", id, encryptedPassword); + throw new IdDeletedException("Rider의 계정이 삭제 상태입니다. 로그인할 수 없습니다."); + } + + + + return riderInfo; + } + + /** + * 라이더의 비밀번호를 변경한다. + * @param id 라이더 아이디 + * @param passwordBeforeChange 변경 전 비밀번호 + * @param passwordAfterChange 변경할 비밀번호 + */ + @Transactional + public void changePassword(@NonNull String id, @NonNull String passwordBeforeChange, + String passwordAfterChange) { + if (isEffective(id, passwordBeforeChange) == false) { + throw passwordMismatchException; + } + + String encryptedPasswordAfter = SHA256Util.encryptSHA256(passwordAfterChange); + riderInfoMapper.updatePassword(id, encryptedPasswordAfter); + } + + /** + * 라이더 계정 정보를 조회한다. + * 일치하는 계정이 없을 시 예외를 발생시킨다. + * @author jun + * @param id 조회할 라이더 계정 아이디 + * @param encryptedPassword 암호화를 진행한 비밀번호 + * @return + */ + public RiderDTO getRiderInfo(@NonNull String id, @NonNull String encryptedPassword) { + RiderDTO riderInfo = riderInfoMapper.findByIdAndPassword(id, encryptedPassword); + + if (Objects.isNull(riderInfo)) { + log.info("회원 정보 없음. id : {}, password : {}", id, encryptedPassword); + throw new IllegalArgumentException("id 또는 password가 일치하는 회원 정보가 없습니다."); + } + + return riderInfo; + } + + /** + * 라이더 계정을 삭제상태로 만든다. + * @param id 삭제할 라이더 아이디 + * @param password 삭제하기 전 유효성 검사를 위한 비밀번호 + */ + @Transactional + public void deleteAccount(@NonNull String id, @NonNull String password) { + if (isEffective(id, password) == false) { + log.info("회원 삭제를 시도하였지만 실패하였습니다. 원인 : 비밀번호 불일치. id : {}", id); + throw passwordMismatchException; + } + + riderInfoMapper.updateStatusAsDeleted(id); + } + + /** + * 아이디와 비밀번호를 기반으로 유효한 아이디인지, 아이디와 비밀번호가 일치하는지 검사한다. + * @author jun + * @param id 검사할 아이디 + * @param password 검사할 비밀번ㄹ호 + * @return + */ + public boolean isEffective(@NonNull String id, @NonNull String password) { + String encryptedPassword = SHA256Util.encryptSHA256(password); + return riderInfoMapper.isExistAndEffectiveByIdAndPassword(id, encryptedPassword); + } + + /** + * 라이더의 메일 주소를 변경한다. + * @author jun + * @param id 메일을 변경할 아이디 + * @param password 유효성 검사를 위한 비밀번호 + * @param mail 변경할 메일 주소 + */ + @Transactional + public void changeMail(@NonNull String id, @NonNull String password, @NonNull String mail) { + if (isEffective(id, password) == false) { + throw passwordMismatchException; + } + + riderInfoMapper.updateMail(id, mail); + } +} diff --git a/src/main/java/com/delfood/utils/RedisKeyFactory.java b/src/main/java/com/delfood/utils/RedisKeyFactory.java index b18e381..cbfa42d 100644 --- a/src/main/java/com/delfood/utils/RedisKeyFactory.java +++ b/src/main/java/com/delfood/utils/RedisKeyFactory.java @@ -2,7 +2,7 @@ public class RedisKeyFactory { public enum Key { - CART, FCM_MEMBER, FCM_OWNER + CART, FCM_MEMBER, FCM_OWNER, FCM_MEMBER_ERROR, FCM_OWNER_ERROR } // 인스턴스화 방지 @@ -33,4 +33,12 @@ public static String generateFcmOwnerKey(String ownerId) { public static String getIdFromKey(String key) { return key.substring(0, key.indexOf(":")); } + + public static String generateFcmMemberErrorKey(String memberId) { + return generateKey(memberId, Key.FCM_MEMBER_ERROR); + } + + public static String generateFcmOwnerErrorKey(String ownerId) { + return generateKey(ownerId, Key.FCM_OWNER_ERROR); + } } diff --git a/src/main/java/com/delfood/utils/SessionUtil.java b/src/main/java/com/delfood/utils/SessionUtil.java index a689943..02e57a4 100644 --- a/src/main/java/com/delfood/utils/SessionUtil.java +++ b/src/main/java/com/delfood/utils/SessionUtil.java @@ -1,11 +1,13 @@ package com.delfood.utils; import javax.servlet.http.HttpSession; +import lombok.NonNull; public class SessionUtil { private static final String LOGIN_MEMBER_ID = "LOGIN_MEMBER_ID"; private static final String LOGIN_OWNER_ID = "LOGIN_OWNER_ID"; + private static final String LOGIN_RIDER_ID = "LOGIN_RIDER_ID"; // 인스턴스화 방지 private SessionUtil() {} @@ -81,6 +83,24 @@ public static void logoutMember(HttpSession session) { public static void logoutOwner(HttpSession session) { session.removeAttribute(LOGIN_OWNER_ID); } + + /** + * 로그인한 라이더의 id를 세션에 저장한다. + * @author jun + * @param session 사용자의 세션 + * @param id 저장할 라이더 아이디 + */ + public static void setLoginRiderId(HttpSession session, @NonNull String id) { + session.setAttribute(LOGIN_RIDER_ID, id); + } + + public static String getLoginRiderId(HttpSession session) { + return (String) session.getAttribute(LOGIN_RIDER_ID); + } + + public static void logoutRider(HttpSession session) { + session.removeAttribute(LOGIN_RIDER_ID); + } diff --git a/src/main/resources/mybatis/mapper/coupon.xml b/src/main/resources/mybatis/mapper/coupon.xml new file mode 100644 index 0000000..2374059 --- /dev/null +++ b/src/main/resources/mybatis/mapper/coupon.xml @@ -0,0 +1,34 @@ + + + + + + INSERT INTO COUPON(name, discount_type, discount_value, end_at) + VALUES(#{name}, #{discountType}, #{discountValue}, #{endAt}) + + SELECT created_at FROM COUPON + WHERE id = (SELECT LAST_INSERT_ID()) + + + + + UPDATE COUPON + SET name = #{name} + AND end_at = #{endAt} + WHERE id = #{id} + + + + UPDATE COUPON + SET status = "DELETED" + WHERE id = #{id} + + + + + diff --git a/src/main/resources/mybatis/mapper/couponIssue.xml b/src/main/resources/mybatis/mapper/couponIssue.xml new file mode 100644 index 0000000..b30c249 --- /dev/null +++ b/src/main/resources/mybatis/mapper/couponIssue.xml @@ -0,0 +1,58 @@ + + + + + + + + + + INSERT INTO COUPON_ISSUE(member_id, coupon_id) + VALUES(#{memberId}, #{couponId}) + + + + UPDATE COUPON_ISSUE + SET status = 'USED', + payment_id = #{paymentId} + WHERE id = #{id} + + + + + + + + + diff --git a/src/main/resources/mybatis/mapper/orders.xml b/src/main/resources/mybatis/mapper/orders.xml index 927c99f..216b253 100644 --- a/src/main/resources/mybatis/mapper/orders.xml +++ b/src/main/resources/mybatis/mapper/orders.xml @@ -14,6 +14,14 @@ + + + + + + + + @@ -30,6 +38,7 @@ + @@ -133,13 +151,22 @@ item_opt.id itemOptionId, opt.id optionId, opt.name optionName, - opt.price optionPrice + opt.price optionPrice, + cpn_isu.id couponIssueId, + cpn.id couponId, + cpn.name couponName, + cpn.discount_type discountType, + cpn.discount_value discountValue, + pay.amount_discount discountPrice FROM ORDERS odr LEFT OUTER JOIN ORDERS_ITEM item ON (odr.id = item.order_id) LEFT OUTER JOIN ORDERS_ITEM_OPTION item_opt ON (item.id = item_opt.order_item_id) INNER JOIN MENU menu ON (menu.id = item.menu_id) LEFT OUTER JOIN OPTION opt ON (opt.menu_id = menu.id) INNER JOIN SHOP shop ON (odr.shop_id = shop.id) INNER JOIN MEMBER member ON (odr.member_id = member.id) + INNER JOIN PAYMENT pay ON (pay.order_id = odr.id) + LEFT OUTER JOIN COUPON_ISSUE cpn_isu ON (pay.id = cpn_isu.payment_id) + LEFT OUTER JOIN COUPON cpn ON (cpn_isu.coupon_id = cpn.id) WHERE member.id = #{memberId} AND odr.id > #{lastViewedOrderId} @@ -164,6 +191,14 @@ + + + + + + + + @@ -178,7 +213,7 @@ @@ -257,4 +301,73 @@ #{option.optionId} ) + + + + UPDATE ORDERS + SET order_status = #{status} + WHERE id = #{orderId} + + + + + + + + UPDATE ORDERS + SET order_status = 'ORDER_APPROVAL', + ex_arrival_time = #{exArrivalTime} + WHERE id = #{orderId} + + + + diff --git a/src/main/resources/mybatis/mapper/payment.xml b/src/main/resources/mybatis/mapper/payment.xml index 0db374a..230a4f0 100644 --- a/src/main/resources/mybatis/mapper/payment.xml +++ b/src/main/resources/mybatis/mapper/payment.xml @@ -1,7 +1,7 @@ - + INSERT INTO PAYMENT (type, amount_payment, pay_time, order_id, status, amount_discount) VALUES diff --git a/src/main/resources/mybatis/mapper/rider.xml b/src/main/resources/mybatis/mapper/rider.xml new file mode 100644 index 0000000..7b54cf9 --- /dev/null +++ b/src/main/resources/mybatis/mapper/rider.xml @@ -0,0 +1,50 @@ + + + + + + + INSERT INTO RIDER(id, password, name, tel, mail) + VALUES (#{id}, #{password}, #{name}, #{tel}, #{mail}) + + + + + + UPDATE RIDER + SET password = #{password}, + updated_at = NOW() + WHERE id = #{id} + + + + UPDATE RIDER + SET status = 'DELETED', + updated_at = NOW() + WHERE id = #{id} + + + + + + UPDATE RIDER + SET mail = #{mail}, + updated_at = NOW() + WHERE id = #{id} + + \ No newline at end of file diff --git a/src/test/java/com/delfood/service/AddressServiceTest.java b/src/test/java/com/delfood/service/AddressServiceTest.java new file mode 100644 index 0000000..96f830b --- /dev/null +++ b/src/test/java/com/delfood/service/AddressServiceTest.java @@ -0,0 +1,164 @@ +package com.delfood.service; + +import static org.junit.Assert.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.BDDMockito.given; + +import com.delfood.controller.reqeust.GetAddressByZipRequest; +import com.delfood.controller.reqeust.GetAddressesByRoadRequest; +import com.delfood.dto.AddressDTO; +import com.delfood.dto.address.Position; +import com.delfood.mapper.AddressMapper; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class AddressServiceTest { + + @InjectMocks + AddressService addressService; + + @Mock + AddressMapper addressMapper; + + public AddressDTO generateAddressDTO() { + AddressDTO addressInfo = new AddressDTO(); + addressInfo.setAdministrativeTownCode("1111051500"); + addressInfo.setAdministrativeTownName("청운효자동"); + addressInfo.setBuildingCenterPointXCoordinate(953035.318387); + addressInfo.setBuildingCenterPointYCoordinate(1954819.846972); + addressInfo.setBuildingCount(9); + addressInfo.setBuildingManagementNumber("1111010100100010000030843"); + addressInfo.setBuildingNameChangeHistory(""); + addressInfo.setBuildingNameForCity("청운벽산빌리지"); + addressInfo.setBuildingNumber(16); + addressInfo.setBuildingSideNumber(14); + addressInfo.setBuildingUseClassification("주택"); + addressInfo.setCityCountryName("종로구"); + addressInfo.setCityCountryNameEng("Jongno-gu"); + addressInfo.setCityName("서울특별시"); + addressInfo.setCityNameEng("Seoul"); + addressInfo.setClassificationApartmentBuildings("2"); + addressInfo.setDetailBuildingName("7동"); + addressInfo.setDetailBuildingNameChangeHistory(""); + addressInfo.setExitXCoordinate(953042.185946); + addressInfo.setExitYCoordinate(1954799.009030); + addressInfo.setGroundFloorNumber(3); + addressInfo.setLivingStatus("1"); + addressInfo.setMobileReasonCode(""); + addressInfo.setRoadName("자하문로36길"); + addressInfo.setRoadNameEng("Jahamun-ro 36-gil"); + addressInfo.setTownCode("1111010100"); + addressInfo.setTownMobileClassification("1"); + addressInfo.setTownName("청운동"); + addressInfo.setTownNameEng("Cheongun-dong"); + addressInfo.setUndergroundFloorNumber(0); + addressInfo.setUndergroundStatus("0"); + addressInfo.setZipCode("03046"); + + return addressInfo; + } + + @Test + public void getTownInfoByShopIdTest_아이디로_주소_조회() { + final Long shopId = 777L; + AddressDTO addressInfo = generateAddressDTO(); + List addressList = new ArrayList(); + addressList.add(addressInfo); + + given(addressMapper.findByShopId(shopId)).willReturn(addressList); + + assertThat(addressService.getTownInfoByShopId(shopId), equalTo(addressList)); + } + + @Test + public void getAddressByZipAddressTest_지번주소로_주소_검색() { + AddressDTO addressInfo = generateAddressDTO(); + List addressList = new ArrayList(); + addressList.add(addressInfo); + + GetAddressByZipRequest searchInfo = new GetAddressByZipRequest(); + searchInfo.setBuildingNameForCity(addressInfo.getBuildingNameForCity()); + searchInfo.setBuildingNumber(addressInfo.getBuildingNumber()); + searchInfo.setBuildingSideNumber(addressInfo.getBuildingSideNumber()); + searchInfo.setTownName(addressInfo.getTownName()); + + given(addressMapper.findByZipName(searchInfo)).willReturn(addressList); + assertThat(addressService.getAddressByZipAddress(searchInfo), equalTo(addressList)); + } + + @Test + public void getAddressByRoadNameTest_도로명주소로_주소_검색() { + AddressDTO addressInfo = generateAddressDTO(); + List addressList = new ArrayList(); + addressList.add(addressInfo); + + GetAddressesByRoadRequest searchInfo = new GetAddressesByRoadRequest(); + searchInfo.setBuildingNameForCity(addressInfo.getBuildingNameForCity()); + searchInfo.setBuildingNumber(addressInfo.getBuildingNumber()); + searchInfo.setBuildingSideNumber(addressInfo.getBuildingSideNumber()); + searchInfo.setRoadName(addressInfo.getRoadName()); + + given(addressMapper.findByRoadName(searchInfo)).willReturn(addressList); + assertThat(addressService.getAddressByRoadName(searchInfo), equalTo(addressList)); + } + + @Test + public void getDistanceMeterTest_거리_계산() { + Position startPosition = Position.builder() + .xPos(0.0) + .yPos(0.0) + .build(); + + Position endPosition = Position.builder() + .xPos(300.0) + .yPos(400.0) + .build(); + + given(addressMapper.findPositionByAddressCode("11111111")).willReturn(startPosition); + given(addressMapper.findPositionByAddressCode("22222222")).willReturn(endPosition); + + assertThat(addressService.getDistanceMeter("11111111", "22222222"), equalTo(500.0)); + } + + @Test + public void deliveryPriceTest_거리기반_배달료_계산() { + final double distances_300 = 300.0; + final double distances_1499_9 = 1499.9; + final double distances_1500 = 1500.0; + final double distances_1500_1 = 1500.1; + final double distances_2500 = 2500.0; + final double distances_1699 = 1699.0; + + assertThat(addressService.deliveryPrice(distances_300), equalTo(2000L)); + assertThat(addressService.deliveryPrice(distances_1499_9), equalTo(2000L)); + assertThat(addressService.deliveryPrice(distances_1500), equalTo(2000L)); + assertThat(addressService.deliveryPrice(distances_1500_1), equalTo(2000L)); + assertThat(addressService.deliveryPrice(distances_2500), equalTo(4000L)); + assertThat(addressService.deliveryPrice(distances_1699), equalTo(2200L)); + } + + @Test + public void deliveryPriceTest_아이디기반_배달료_계산() { + Position memberPosition = Position.builder() + .xPos(0.0) + .yPos(0.0) + .build(); + + Position shopPosition = Position.builder() + .xPos(3000.0) + .yPos(4000.0) + .build(); + + given(addressMapper.findPositionByMemberId("eric")).willReturn(memberPosition); + given(addressMapper.findPositionByShopId(555L)).willReturn(shopPosition); + + assertThat(addressService.deliveryPrice("eric", 555L), equalTo(9000L)); + } + +} diff --git a/src/test/java/com/delfood/service/CartServiceTest.java b/src/test/java/com/delfood/service/CartServiceTest.java index b58df32..c46e09a 100644 --- a/src/test/java/com/delfood/service/CartServiceTest.java +++ b/src/test/java/com/delfood/service/CartServiceTest.java @@ -1,8 +1,19 @@ package com.delfood.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.equalTo; import static org.mockito.BDDMockito.given; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import com.delfood.dao.CartDao; +import com.delfood.dto.ItemDTO; +import com.delfood.dto.ShopDTO; +import com.delfood.dto.ItemDTO.CacheMenuDTO; +import com.delfood.dto.ItemDTO.CacheOptionDTO; +import com.delfood.dto.ItemDTO.CacheShopDTO; +import com.delfood.error.exception.cart.DuplicateItemException; +import com.google.common.collect.Lists; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -12,15 +23,147 @@ @RunWith(MockitoJUnitRunner.class) public class CartServiceTest { @InjectMocks - CartService service; + CartService cartService; @Mock - CartDao dao; + CartDao cartDao; + + /** + * id에 따른 Item을 생산하여 리턴한다. + * @author jun + * @param id 해당 아이디를 기반으로 메뉴와 옵션을 생산하여 아이템을 제작한다. + * @return + */ + public ItemDTO generateItem(long id) { + final long menuId = id * 111; + final long shopId = 222L; + final long menuPrice = 11000L; + final long optionId = id; + final long[] optionPrices = {100L, 200L, 300L}; + + CacheMenuDTO menuInfo = new CacheMenuDTO(menuId, "테스트 메뉴 " + menuId, menuPrice); + + List options = new ArrayList(); + CacheOptionDTO optionInfo1 = new CacheOptionDTO(optionId, menuId + " 옵션 1", optionPrices[0]); + CacheOptionDTO optionInfo2 = + new CacheOptionDTO(optionId * 2L, menuId + " 옵션 2", optionPrices[1]); + CacheOptionDTO optionInfo3 = + new CacheOptionDTO(optionId * 3L, menuId + " 옵션 3", optionPrices[2]); + options.add(optionInfo1); + options.add(optionInfo2); + options.add(optionInfo3); + + CacheShopDTO shopInfo = new CacheShopDTO(shopId, "테스트 매장 이름"); + + ItemDTO itemInfo = new ItemDTO(menuInfo, options, 1, menuPrice + 600L, shopInfo); + + return itemInfo; + } + + public ItemDTO generateItemAnotherShop() { + CacheMenuDTO menuInfo = new CacheMenuDTO(222L, "테스트 메뉴 222", 11000L); + + List options = new ArrayList(); + CacheOptionDTO optionInfo1 = new CacheOptionDTO(2L, "222 옵션 1", 100L); + CacheOptionDTO optionInfo2 = new CacheOptionDTO(2L, "222 옵션 2", 200L); + CacheOptionDTO optionInfo3 = new CacheOptionDTO(2L, "222 옵션 3", 300L); + options.add(optionInfo1); + options.add(optionInfo2); + options.add(optionInfo3); + + CacheShopDTO shopInfo = new CacheShopDTO(123L, "테스트 매장 이름"); + + ItemDTO itemInfo = new ItemDTO(menuInfo, options, 1, 11600L, shopInfo); + + return itemInfo; + } + + @Test + public void addOrdersItemTest_장바구니에_메뉴_추가() { + ItemDTO item1 = generateItem(1L); + ItemDTO item2 = generateItem(2L); + given(cartDao.findPeekByMemberId("eric")).willReturn(item1); + cartService.addOrdersItem(item2, "eric"); + } + + @Test(expected = IllegalArgumentException.class) + public void addOrdersItemTest_장바구니에_다른매장_메뉴_추가() { + ItemDTO item1 = generateItem(1L); + ItemDTO item2 = generateItemAnotherShop(); + given(cartDao.findPeekByMemberId("eric")).willReturn(item1); + cartService.addOrdersItem(item2, "eric"); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void addOrdersItemTest_너무많은메뉴추가() { + given(cartDao.findPeekByMemberId("eric")).willReturn(generateItem(1L)); + given(cartDao.findAllByMemberId("eric")).willReturn(Arrays.asList( + new ItemDTO[] {generateItem(1L), generateItem(1L), generateItem(1L), generateItem(1L), + generateItem(1L), generateItem(1L), generateItem(1L), generateItem(1L), + generateItem(1L), generateItem(1L), generateItem(1L), generateItem(1L)})); + cartService.addOrdersItem(generateItem(2L), "eric"); + } + @Test(expected = DuplicateItemException.class) + public void addOrdersItemTest_같메뉴추가() { + given(cartDao.findPeekByMemberId("eric")).willReturn(generateItem(1L)); + given(cartDao.findAllByMemberId("eric")).willReturn(Arrays.asList( + new ItemDTO[] {generateItem(1L)})); + cartService.addOrdersItem(generateItem(1L), "eric"); + } - // 로직이 확정되면 테스트코드를 다시 작성할 예정입니다 @Test - public void mockTest() { + public void deleteCartMenuTest_장바구니_메뉴_삭제_인덱스() { + given(cartDao.getMenuCount("eric")).willReturn(5L); + given(cartDao.deleteByMemberIdAndIndex("eric", 1L)).willReturn(true); + cartService.deleteCartMenu("eric", 1L); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void deleteCartMenuTest_장바구니_메뉴_삭제_인덱스_초과() { + given(cartDao.getMenuCount("eric")).willReturn(5L); + cartService.deleteCartMenu("eric", 6L); + } + + @Test(expected = RuntimeException.class) + public void deleteCartMenuTest_장바구니_메뉴_삭제_redis에러() { + given(cartDao.getMenuCount("eric")).willReturn(5L); + given(cartDao.deleteByMemberIdAndIndex("eric", 1L)).willReturn(false); + cartService.deleteCartMenu("eric", 1L); + } + + @Test + public void containsEqualItemTest_장바구니_동일아이템_포함여부_검사() { + given(cartDao.findAllByMemberId("eric")) + .willReturn(Arrays.asList(new ItemDTO[] {generateItem(1L)})); + assertThat(cartService.containsEqualItem("eric", generateItem(1L))).isEqualTo(true); + assertThat(cartService.containsEqualItem("eric", generateItem(2L))).isEqualTo(false); + } + + @Test + public void allPriceTest_장바구니_총가격_계산() { + given(cartDao.findAllByMemberId("eric")) + .willReturn(Arrays.asList(new ItemDTO[] {generateItem(1L), generateItem(2L)})); + assertThat(cartService.allPrice("eric")).isEqualTo(23200L); } + + @Test + public void priceTest_아이템_가격계산() { + ItemDTO itemInfo = generateItem(1L); + assertThat(cartService.price(itemInfo)).isEqualTo(11600L); + } + + @Test + public void menuPriceTest_아이템_메뉴만_가격계산() { + ItemDTO itemInfo = generateItem(1L); + assertThat(CartService.menuPrice(itemInfo)).isEqualTo(11000L); + } + + @Test + public void menuPriceTest_아이템_옵션만_가격계산() { + ItemDTO itemInfo = generateItem(1L); + assertThat(CartService.optionsPrice(itemInfo)).isEqualTo(600L); + } + } diff --git a/src/test/java/com/delfood/service/CouponIssueServiceTest.java b/src/test/java/com/delfood/service/CouponIssueServiceTest.java new file mode 100644 index 0000000..8c6804b --- /dev/null +++ b/src/test/java/com/delfood/service/CouponIssueServiceTest.java @@ -0,0 +1,47 @@ +package com.delfood.service; + +import static org.mockito.BDDMockito.given; +import com.delfood.error.exception.DuplicateException; +import com.delfood.mapper.CouponIssueMapper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CouponIssueServiceTest { + + @InjectMocks + private CouponIssueService couponIssueService; + + @Mock + private CouponIssueMapper couponIssueMapper; + + @Mock + private CouponService couponService; + + @Test + public void createCouponIssue_쿠폰_발급_성공() { + String memberId = "eric"; + long couponId = 1L; + + given(couponIssueMapper.countCouponIssueByMemberIdAndCouponId(memberId, couponId)) + .willReturn(0); + given(couponIssueMapper.insertCouponIssue(memberId, couponId)).willReturn(1); + + couponIssueService.createCouponIssue(memberId, couponId); + } + + @Test(expected = DuplicateException.class) + public void createCouponIssue_쿠폰_발급_실패_재발급() { + String memberId = "eric"; + long couponId = 1L; + + given(couponIssueMapper.countCouponIssueByMemberIdAndCouponId(memberId, couponId)) + .willReturn(1); + + couponIssueService.createCouponIssue(memberId, couponId); + } + +} diff --git a/src/test/java/com/delfood/service/CouponServiceTest.java b/src/test/java/com/delfood/service/CouponServiceTest.java new file mode 100644 index 0000000..84e686c --- /dev/null +++ b/src/test/java/com/delfood/service/CouponServiceTest.java @@ -0,0 +1,129 @@ +package com.delfood.service; + +import static org.mockito.BDDMockito.given; +import static org.assertj.core.api.Assertions.assertThat; + +import com.delfood.dto.CouponDTO; +import com.delfood.dto.CouponDTO.DiscountType; +import com.delfood.dto.CouponDTO.Status; +import com.delfood.error.exception.coupon.IssuedCouponExistException; +import com.delfood.mapper.CouponMapper; +import java.time.LocalDateTime; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CouponServiceTest { + + @InjectMocks + private CouponService couponService; + + @Mock + private CouponMapper couponMapper; + + @Mock + CouponIssueService couponIssueService; + + /** + * 정상적으로 작동할 수 있는 쿠폰 DTO를 새로 생성하여 반환한다. + * @author jun + * @return + */ + public static CouponDTO generateCoupon() { + CouponDTO couponInfo = new CouponDTO(); + + couponInfo.setId(111L); + couponInfo.setName("Test Coupon"); + couponInfo.setDiscountType(DiscountType.PERCENT); + couponInfo.setDiscountValue(10L); + couponInfo.setCreatedAt(LocalDateTime.now().minusDays(1)); // 항상 오늘보다 하루 전 만들어진 쿠폰으로 설정한다. + couponInfo.setUpdatedAt(LocalDateTime.now().minusDays(1)); + couponInfo.setEndAt(LocalDateTime.now().plusDays(1)); // 항상 오늘보다 하루 뒤 종료하도록 설정한다. + couponInfo.setStatus(Status.DEFAULT); + + return couponInfo; + } + + @Test + public void addCouponTest_쿠폰_추가_성공() { + CouponDTO couponInfo = generateCoupon(); + given(couponMapper.insertCoupon(couponInfo)).willReturn(1L); + + couponService.addCoupon(couponInfo); + } + + @Test(expected = IllegalStateException.class) + public void addCouponTest_쿠폰_추가_실패_종료일_설정_오류() { + CouponDTO couponInfo = generateCoupon(); + couponInfo.setEndAt(couponInfo.getCreatedAt().minusDays(1)); + given(couponMapper.insertCoupon(couponInfo)).willReturn(1L); + + couponService.addCoupon(couponInfo); + } + + @Test(expected = IllegalArgumentException.class) + public void addCouponTest_쿠폰_추가_실패_할인율_101퍼센트() { + CouponDTO discountValueErrorCouponInfo = generateCoupon(); + discountValueErrorCouponInfo.setDiscountValue(101L); + + couponService.addCoupon(discountValueErrorCouponInfo); + } + + @Test(expected = IllegalArgumentException.class) + public void addCouponTest_쿠폰_추가_실패_할인율_0미만퍼센트() { + CouponDTO discountValueErrorCouponInfo = generateCoupon(); + discountValueErrorCouponInfo.setDiscountValue(-1L); + + couponService.addCoupon(discountValueErrorCouponInfo); + } + + @Test + public void updateCouponNameAndEndAtTest_쿠폰_업데이트_성공() { + String updateName = "new Test Coupon Name"; + LocalDateTime updateEndAt = LocalDateTime.now().plusDays(1L); + given(couponIssueService.isIssued(1L)).willReturn(false); + given(couponMapper.updateCouponNameAndEndAt(1L, updateName, + updateEndAt)).willReturn(1); + + couponService.updateCouponNameAndEndAt(1L, updateName, updateEndAt); + } + + @Test(expected = IssuedCouponExistException.class) + public void updateCouponNameAndEndAtTest_쿠폰_업데이트_실패_이미발행() { + String updateName = "new Test Coupon Name"; + given(couponIssueService.isIssued(1L)).willReturn(true); + + couponService.updateCouponNameAndEndAt(1L, updateName, LocalDateTime.now().plusDays(1L)); + } + + @Test + public void deleteCouponTest_쿠폰_삭제_성공() { + given(couponIssueService.isIssued(1L)).willReturn(false); + given(couponMapper.deleteCoupon(1L)).willReturn(1); + + couponService.deleteCoupon(1L); + } + + @Test(expected = IssuedCouponExistException.class) + public void deleteCouponTest_쿠폰_삭제_실패_이미발행() { + given(couponIssueService.isIssued(1L)).willReturn(true); + + couponService.deleteCoupon(1L); + } + + @Test + public void getAvaliableCouponsTest_사용가능_쿠폰_조회_성공() { + given(couponMapper.findByEndAtGreaterThanNow()) + .willReturn(Arrays.asList(new CouponDTO[] {generateCoupon(), generateCoupon()})); + + assertThat(couponService.getAvaliableCoupons()) + .isNotEmpty() + .hasSize(2); + } + + +} diff --git a/src/test/java/com/delfood/service/MenuServiceTest.java b/src/test/java/com/delfood/service/MenuServiceTest.java new file mode 100644 index 0000000..b1e703b --- /dev/null +++ b/src/test/java/com/delfood/service/MenuServiceTest.java @@ -0,0 +1,135 @@ +package com.delfood.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; + +import com.delfood.dto.MenuDTO; +import com.delfood.dto.MenuDTO.Status; +import com.delfood.dto.OptionDTO; +import com.delfood.mapper.MenuMapper; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class MenuServiceTest { + + @InjectMocks + MenuService service; + + @Mock + MenuMapper mapper; + + public MenuDTO generateMenu() { + MenuDTO menu = new MenuDTO(); + menu.setId(1L); + menu.setMenuGroupId(1L); + menu.setContent("Test Menu Content"); + menu.setOptionList(new ArrayList()); + menu.setPhoto("Test Photo URL"); + menu.setPrice(12000L); + menu.setPriority(1L); + menu.setStatus(Status.DEFAULT); + menu.setCreatedAt(LocalDateTime.now()); + menu.setUpdatedAt(LocalDateTime.now()); + return menu; + } + + @Test + public void getMenuInfoTest_메뉴_조회_성공() { + MenuDTO menu = generateMenu(); + given(mapper.findById(1L)).willReturn(menu); + given(mapper.findById(999L)).willReturn(null); + assertThat(service.getMenuInfo(1L)).isEqualTo(menu); + assertThat(service.getMenuInfo(999L)).isNull(); + } + + @Test + public void addMenuTest_메뉴_추가_성공() { + MenuDTO menu = generateMenu(); + given(mapper.insertMenu(menu)).willReturn(menu.getId()); + service.addMenu(menu); + } + + @Test + public void deleteMenu_메뉴_삭제_성공() { + given(mapper.deleteMenu(1L)).willReturn(1); + service.deleteMenu(1L); + } + + @Test(expected = RuntimeException.class) + public void deleteMenu_메뉴_삭제_실패() { + given(mapper.deleteMenu(1L)).willReturn(0); + service.deleteMenu(1L); + } + + @Test + public void checkMenuTest_메뉴존재_체크_성공() { + given(mapper.checkMenu(1L, 1L)).willReturn(1); + service.checkMenu(1L, 1L); + } + + @Test(expected = RuntimeException.class) + public void checkMenuTest_메뉴존재_체크_없음() { + given(mapper.checkMenu(1L, 1L)).willReturn(0); + service.checkMenu(1L, 1L); + } + + @Test(expected = RuntimeException.class) + public void checkMenuTest_메뉴존재_체크_실패() { + given(mapper.checkMenu(1L, 1L)).willReturn(999); + service.checkMenu(1L, 1L); + } + + @Test + public void updateMenuPriorityTest_메뉴_순서_변경_성공() { + given(mapper.totalCount(1L)).willReturn(3); + given(mapper.updateMenuPriority(anyLong(), anyInt())).willReturn(1); + List idList = LongStream.of(1,2,3).boxed().collect(Collectors.toList()); + + service.updateMenuPriority(1L, idList); + } + + @Test(expected = RuntimeException.class) + public void updateMenuPriorityTest_메뉴_순서_변경_실패() { + given(mapper.totalCount(1L)).willReturn(0); + List idList = LongStream.of(1,2,3).boxed().collect(Collectors.toList()); + + service.updateMenuPriority(1L, idList); + } + + @Test(expected = RuntimeException.class) + public void updateMenuPriorityTest_메뉴_순서_변경_실패2() { + given(mapper.totalCount(1L)).willReturn(3); + given(mapper.updateMenuPriority(anyLong(), anyInt())).willReturn(0); + List idList = LongStream.of(1,2,3).boxed().collect(Collectors.toList()); + + service.updateMenuPriority(1L, idList); + } + + @Test + public void updateMenuTest_메뉴_정보_수정_성공() { + MenuDTO menu = generateMenu(); + given(mapper.updateMenu(menu)).willReturn(1); + service.updateMenu(menu); + } + + @Test(expected = RuntimeException.class) + public void updateMenuTest_메뉴_정보_수정_실패() { + MenuDTO menu = generateMenu(); + given(mapper.updateMenu(menu)).willReturn(0); + service.updateMenu(menu); + } + + + +} diff --git a/src/test/java/com/delfood/service/OptionServiceTest.java b/src/test/java/com/delfood/service/OptionServiceTest.java new file mode 100644 index 0000000..87e1e50 --- /dev/null +++ b/src/test/java/com/delfood/service/OptionServiceTest.java @@ -0,0 +1,98 @@ +package com.delfood.service; + +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.given; + +import com.delfood.dto.OptionDTO; +import com.delfood.dto.OptionDTO.Status; +import com.delfood.mapper.OptionMapper; +import java.util.ArrayList; +import java.util.List; +import lombok.extern.log4j.Log4j2; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +@Log4j2 +public class OptionServiceTest { + + @InjectMocks + OptionService service; + + @Mock + OptionMapper mapper; + + public OptionDTO generateOption() { + OptionDTO option = new OptionDTO(); + option.setId(1L); + option.setMenuId(1L); + option.setName("Test Option Name"); + option.setPrice(10000L); + option.setStatus(Status.DEFAULT); + return option; + } + + @Test + public void addOptionTest_옵션_추가_성공() { + OptionDTO option = generateOption(); + given(mapper.insertOption(option)).willReturn(1); + service.addOption(option); + } + + @Test + public void addOptionTest_옵션_추가_실패_null() { + try { + OptionDTO option = generateOption(); + option.setName(null); + service.addOption(option); + log.info("name null check 실패"); + fail(); + } catch (RuntimeException e) { + log.info("name null check 성공"); + } + + try { + OptionDTO option = generateOption(); + option.setPrice(null); + service.addOption(option); + log.info("price null check 실패"); + fail(); + } catch (RuntimeException e) { + log.info("price null check 성공"); + } + + try { + OptionDTO option = generateOption(); + option.setMenuId(null); + service.addOption(option); + log.info("menuId null check 실패"); + fail(); + } catch (RuntimeException e) { + log.info("menuId null check 성공"); + } + } + + @Test(expected = RuntimeException.class) + public void addOptionTest_옵션_추가_실패_DB에러() { + OptionDTO option = generateOption(); + given(mapper.insertOption(option)).willReturn(0); + service.addOption(option); + } + + @Test + public void addOptionList_옵션_리스트_추가_성공() { + List options = new ArrayList(); + for (long l = 1; l <= 3; l++) { + OptionDTO option = generateOption(); + option.setId(l); + options.add(option); + } + + given(mapper.insertOptionList(options, 1L)).willReturn(1); + service.addOptionList(options, 1L); + } + +} diff --git a/src/test/java/com/delfood/service/OwnerServiceTest.java b/src/test/java/com/delfood/service/OwnerServiceTest.java index 33483bc..5ced618 100644 --- a/src/test/java/com/delfood/service/OwnerServiceTest.java +++ b/src/test/java/com/delfood/service/OwnerServiceTest.java @@ -1,13 +1,10 @@ package com.delfood.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import com.delfood.dto.OwnerDTO; +import com.delfood.error.exception.DuplicateException; import com.delfood.mapper.OwnerMapper; import com.delfood.utils.SHA256Util; import org.junit.Test; @@ -26,38 +23,203 @@ public class OwnerServiceTest { @Mock // mock 생성 OwnerMapper mapper; + /** + * owner 정보 생성. + * @return + */ + public OwnerDTO generateOwner() { + return OwnerDTO.builder() + .id("ljy2134") + .password(SHA256Util.encryptSHA256("2134")) + .name("이진영") + .mail("asdf@naver.com") + .tel("010-3333-3333") + .build(); + } + /** * 회원가입 성공 테스트. */ @Test public void signUp_success() { - OwnerDTO ownerInfo = OwnerDTO.builder() - .id("ljy2134") - .password(SHA256Util.encryptSHA256("2134")) - .name("이진영") - .mail("asdf@naver.com") - .tel("010-3333-3333") - .build(); + OwnerDTO ownerInfo = generateOwner(); given(mapper.insertOwner(ownerInfo)).willReturn(1); + given(mapper.idCheck(ownerInfo.getId())).willReturn(0); + service.signUp(ownerInfo); } /** - * 회원가입 실패 테스트. + * 회원가입 실패 테스트. (DB insert 실패) */ @Test(expected = RuntimeException.class) public void signUp_fail() { - OwnerDTO ownerInfo = OwnerDTO.builder() - .id("ljy2134") - .password(SHA256Util.encryptSHA256("2134")) - .name("이진영") - .mail("asdf@naver.com") - .tel("010-3333-3333") - .build(); + OwnerDTO ownerInfo = generateOwner(); + given(mapper.idCheck(ownerInfo.getId())).willReturn(0); given(mapper.insertOwner(ownerInfo)).willReturn(0); + service.signUp(ownerInfo); } + /** + * 회원가입 실패 테스트. (아이디 중복 발생) + */ + @Test(expected = RuntimeException.class) + public void signUp_fail2() { + OwnerDTO ownerInfo = generateOwner(); + + given(mapper.idCheck(ownerInfo.getId())).willReturn(1); + + service.signUp(ownerInfo); + } + + /** + * 아이디 중복 체크 성공 테스트. + */ + @Test + public void isDuplicatedId_success() { + String duplicatedId = "duplicatedId"; + String noDuplicatedId = "noDuplicatedId"; + given(mapper.idCheck(duplicatedId)).willReturn(1); + given(mapper.idCheck(noDuplicatedId)).willReturn(0); + + assertThat(service.isDuplicatedId(duplicatedId)).isTrue(); + assertThat(service.isDuplicatedId(noDuplicatedId)).isFalse(); + } + + /** + * 사장 정보 조회 성공 테스트. + */ + @Test + public void getOwner_success() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String password = ownerInfo.getPassword(); + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(password))) + .willReturn(ownerInfo); + given(mapper.findById(id)).willReturn(ownerInfo); + + assertThat(service.getOwner(id, password)).isEqualTo(ownerInfo); + assertThat(service.getOwner(id)).isEqualTo(ownerInfo); + } + + /** + * 사장 이메일, 전화번호 수정 성공 테스트. + */ + @Test + public void updateOwnerMailAndTel_success() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String password = ownerInfo.getPassword(); + String mail = ownerInfo.getMail(); + String tel = ownerInfo.getTel(); + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(password))).willReturn(ownerInfo); + given(mapper.updateMailAndTel(id, mail, tel)).willReturn(1); + + service.updateOwnerMailAndTel(id, password, mail, tel); + } + + /** + * 사장 이메일, 전화번호 수정 실패 테스트. (아이디, 패스워드 검증 실패) + */ + @Test(expected = IllegalArgumentException.class) + public void updateOwnerMailAndTel_fail() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String password = ownerInfo.getPassword(); + String mail = ownerInfo.getMail(); + String tel = ownerInfo.getTel(); + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(password))).willReturn(null); + + service.updateOwnerMailAndTel(id, password, mail, tel); + } + + /** + * 사장 이메일, 전화번호 수정 실패 테스트. (DB update 실패) + */ + @Test(expected = RuntimeException.class) + public void updateOwnerMailAndTel_fail2() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String password = ownerInfo.getPassword(); + String mail = ownerInfo.getMail(); + String tel = ownerInfo.getTel(); + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(password))).willReturn(ownerInfo); + given(mapper.updateMailAndTel(id, mail, tel)).willReturn(0); + + service.updateOwnerMailAndTel(id, password, mail, tel); + } + + + /** + * 사장 비밀번호 수정 성공 테스트. + */ + @Test + public void updateOwnerPassword_success() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String beforePassword = ownerInfo.getPassword(); + String afterPassword = "asdfasdf"; + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(beforePassword))) + .willReturn(ownerInfo); + given(mapper.updatePassword(id, SHA256Util.encryptSHA256(afterPassword))).willReturn(1); + + service.updateOwnerPassword(id, beforePassword, afterPassword); + } + + /** + * 사장 비밀번호 수정 실패 테스트. (아이디와 기존 패스워드 매칭 실패) + */ + @Test(expected = IllegalArgumentException.class) + public void updateOwnerPassword_fail() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String beforePassword = "strangePassword"; + String afterPassword = "asdfasdf"; + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(beforePassword))) + .willReturn(null); + + service.updateOwnerPassword(id, beforePassword, afterPassword); + } + + /** + * 사장 비밀번호 수정 실패 테스트. (변경 전 패스워드와 변경 후 패스워드가 일치) + */ + @Test(expected = DuplicateException.class) + public void updateOwnerPassword_fail2() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String beforePassword = ownerInfo.getPassword(); + String afterPassword = ownerInfo.getPassword(); + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(beforePassword))) + .willReturn(ownerInfo); + + service.updateOwnerPassword(id, beforePassword, afterPassword); + } + + /** + * 사장 비밀번호 수정 실패 테스트. (변경 전 패스워드와 변경 후 패스워드가 일치) + */ + @Test(expected = RuntimeException.class) + public void updateOwnerPassword_fail3() { + OwnerDTO ownerInfo = generateOwner(); + String id = ownerInfo.getId(); + String beforePassword = ownerInfo.getPassword(); + String afterPassword = "afterPassword"; + + given(mapper.findByIdAndPassword(id, SHA256Util.encryptSHA256(beforePassword))) + .willReturn(ownerInfo); + given(mapper.updatePassword(id, SHA256Util.encryptSHA256(afterPassword))).willReturn(0); + + service.updateOwnerPassword(id, beforePassword, afterPassword); + } } diff --git a/src/test/java/com/delfood/service/ShopServiceTest.java b/src/test/java/com/delfood/service/ShopServiceTest.java new file mode 100644 index 0000000..47e6d41 --- /dev/null +++ b/src/test/java/com/delfood/service/ShopServiceTest.java @@ -0,0 +1,112 @@ +package com.delfood.service; + +import static org.mockito.BDDMockito.given; + +import com.delfood.dto.ShopDTO; +import com.delfood.dto.ShopDTO.DeliveryType; +import com.delfood.dto.ShopDTO.OrderType; +import com.delfood.dto.ShopDTO.Status; +import com.delfood.mapper.DeliveryLocationMapper; +import com.delfood.mapper.ShopMapper; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ShopServiceTest { + + @InjectMocks + private ShopService shopService; + + @Mock + private ShopMapper shopMapper; + + @Mock + private WorkService workService; + + @Mock + private AddressService addressService; + + @Mock + private DeliveryLocationMapper deliveryLocationMapper; + + /** + * shop 정보 생성. + * @return + */ + public ShopDTO generateShop() { + return ShopDTO.builder() + .id(999L) + .name("교촌치킨") + .deliveryType(DeliveryType.COMPANY_DELIVERY) + .signatureMenuId(1L) + .tel("02-2222-2222") + .addressCode("1111010100100010000031108") + .addressDetail("교촌치킨") + .bizNumber("111-11-11111") + .info("허니콤보 맛집") + .minOrderPrice(10000L) + .notice("배달료 3000원 붙습니다") + .operatingTime("11:00 ~ 02:00") + .ownerId("ljy2134") + .orderType(OrderType.THIS_PAYMENT) + .originInfo("닭 : 국내산") + .status(Status.DEFAULT) + .build(); + } + + /** + * 매장 추가 성공 테스트. + */ + @Test + public void addShopSuccess() { + ShopDTO shopInfo = generateShop(); + + given(shopMapper.insertShop(shopInfo)).willReturn(1); + + shopService.addShop(shopInfo); + } + + /** + * 시그니처 메뉴가 없는 아이디 일 때 실패 테스트. + */ + @Test(expected = RuntimeException.class) + public void addShopFailBeacauseMenuDoesNotExist() { + ShopDTO shopInfo = generateShop(); + shopInfo.setSignatureMenuId(-1L); + + given(shopMapper.insertShop(shopInfo)).willReturn(0); + + shopService.addShop(shopInfo); + } + + /** + * Owner가 없는 아이디 일 때 실패 테스트. + */ + @Test(expected = RuntimeException.class) + public void addShopFailBeacauseOwnerDoesNotExist() { + ShopDTO shopInfo = generateShop(); + shopInfo.setOwnerId("noExistId"); + + given(shopMapper.insertShop(shopInfo)).willReturn(0); + + shopService.addShop(shopInfo); + } + + /** + * address가 없는 아이디 일 때 실패 테스트. + */ + @Test(expected = RuntimeException.class) + public void addShopFailBeacauseAddressDoesNotExist() { + ShopDTO shopInfo = generateShop(); + shopInfo.setAddressCode("0000000000000000000000000"); + + given(shopMapper.insertShop(shopInfo)).willReturn(0); + + shopService.addShop(shopInfo); + } + +}