Skip to content

Commit

Permalink
feat: 알림톡 기능 구현 (#286)
Browse files Browse the repository at this point in the history
* feat: 알림톡 기능 구현 / 예약등록 알림톡 적용

* feat: 예약 확정 알림톡 적용

* feat: 예약 확정 + 초대장 알림톡 적용

* feat: 예약 취소 알림톡 구현

* feat: 예약 취소 알림톡 구현

* fix: 예약 확정 로직 수정
  • Loading branch information
sejineer authored Mar 5, 2024
1 parent 90cb35c commit a31611b
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.shallwe.domain.reservation.application;

import com.shallwe.domain.reservation.dto.response.DeleteReservationRes;
import com.shallwe.domain.reservation.dto.request.OwnerReservationCreate;
import com.shallwe.domain.reservation.dto.response.ReservationResponse;
import com.shallwe.domain.reservation.dto.request.UserReservationCreate;
Expand All @@ -14,6 +13,6 @@ public interface ReservationManipulationService {
List<ReservationResponse> addOwnerReservation(OwnerReservationCreate ownerReservationCreate, UserPrincipal userPrincipal);
ReservationResponse addUserReservation(UserReservationCreate reservationRequest, UserPrincipal userPrincipal) throws Exception;
ReservationResponse updateReservation(UpdateReservationReq updateReq, UserPrincipal userPrincipal);
DeleteReservationRes deleteReservation(Long id);
void cancelReservation(UserPrincipal userPrincipal, Long reservationId) throws Exception;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.shallwe.domain.reservation.application;

import static com.shallwe.domain.reservation.domain.ReservationStatus.*;
import static com.shallwe.domain.reservation.domain.ReservationStatus.WAITING;

import com.shallwe.domain.common.Status;
Expand All @@ -9,16 +10,14 @@
import com.shallwe.domain.reservation.domain.Reservation;
import com.shallwe.domain.reservation.domain.ReservationStatus;
import com.shallwe.domain.reservation.domain.repository.ReservationRepository;
import com.shallwe.domain.reservation.dto.response.DeleteReservationRes;
import com.shallwe.domain.reservation.dto.request.OwnerReservationCreate;
import com.shallwe.domain.reservation.dto.response.ReservationResponse;
import com.shallwe.domain.reservation.dto.request.UserReservationCreate;
import com.shallwe.domain.reservation.dto.request.UpdateReservationReq;
import com.shallwe.domain.reservation.exception.InvalidReservationException;
import com.shallwe.domain.reservation.exception.InvalidSenderException;
import com.shallwe.domain.reservation.exception.InvalidReceiverException;
import com.shallwe.domain.reservation.exception.*;
import com.shallwe.domain.user.domain.User;
import com.shallwe.domain.user.domain.repository.UserRepository;
import com.shallwe.domain.user.exception.InvalidUserException;
import com.shallwe.global.config.security.token.UserPrincipal;

import java.util.List;
Expand All @@ -39,6 +38,7 @@ public class ReservationManipulationServiceImpl implements ReservationManipulati
private final UserRepository userRepository;
private final NaverSmsClient naverSmsClient;

@Override
@Transactional
public List<ReservationResponse> addOwnerReservation(OwnerReservationCreate ownerReservationCreate, UserPrincipal userPrincipal) {
ExperienceGift experienceGift = experienceGiftRepository.findById(ownerReservationCreate.getExperienceGiftId())
Expand All @@ -51,6 +51,7 @@ public List<ReservationResponse> addOwnerReservation(OwnerReservationCreate owne
.collect(Collectors.toList());
}

@Override
@Transactional
public ReservationResponse addUserReservation(UserReservationCreate reservationRequest, UserPrincipal userPrincipal) throws Exception {
User sender = userRepository.findById(userPrincipal.getId())
Expand All @@ -67,16 +68,17 @@ public ReservationResponse addUserReservation(UserReservationCreate reservationR
.orElseThrow(InvalidReservationException::new);

if (reservation.getReservationStatus().equals(WAITING)) {
reservation.updateStatus(ReservationStatus.BOOKED);
reservation.updateStatus(BOOKED);
reservation.updateUserReservationRequest(reservationRequest, sender, receiver);
naverSmsClient.sendReservationApply(sender, receiver, experienceGift, reservation);
naverSmsClient.sendApply(receiver, experienceGift, reservation);
experienceGift.addReservationCount();
} else {
throw new InvalidReservationException();
}
return ReservationResponse.toDtoUser(reservation);
}

@Override
@Transactional
public ReservationResponse updateReservation(UpdateReservationReq updateReq, UserPrincipal userPrincipal) {
Reservation updateReservation = reservationRepository.findById(
Expand All @@ -89,12 +91,28 @@ public ReservationResponse updateReservation(UpdateReservationReq updateReq, Use
return ReservationResponse.toDtoUser(updateReservation);
}

@Override
@Transactional
public DeleteReservationRes deleteReservation(Long id) {
Reservation reservation = reservationRepository.findById(id)
public void cancelReservation(final UserPrincipal userPrincipal, final Long reservationId) throws Exception {
Reservation reservation = reservationRepository.findReservationById(reservationId)
.orElseThrow(InvalidReservationException::new);
reservation.updateStatus(Status.DELETE);
return DeleteReservationRes.toDTO();

User user = userRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidUserException::new);

if (!reservation.getSender().getId().equals(user.getId()))
throw new UserReservationMismatchException();

if(reservation.getReservationStatus().equals(BOOKED)) { // BOOKED 상태일 때 WAITING으로 변경
reservation.updateStatus(WAITING);
naverSmsClient.sendCancel(reservation);
reservation.clearReservation();
} else if(reservation.getReservationStatus().equals(CONFIRMED)) { // CONFIRMED 상태일 때 CANCELLED로 변경
reservation.updateStatus(CANCELLED);
naverSmsClient.sendCancel(reservation);
} else { // 예약 상태가 BOOKED나 CONFIRMED가 아닐 경우
throw new NotAvailableReservationStatusException();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,13 @@ public void updateStatus(ReservationStatus status) {
this.reservationStatus = status;
}

public void clearReservation() {
this.sender = null;
this.receiver = null;
this.phoneNumber = null;
this.invitationComment = null;
this.persons = null;
this.invitationImg = null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ public interface ReservationRepository extends JpaRepository<Reservation, Long>,

Optional<List<Reservation>> findAllByExperienceGiftAndDate(ExperienceGift experienceGift, LocalDate date);

@EntityGraph(attributePaths = {"experienceGift", "experienceGift.shopOwner", "sender", "receiver"})
Optional<Reservation> findReservationById(Long reservationId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.shallwe.domain.reservation.exception;

public class NotAvailableReservationStatusException extends RuntimeException {

public NotAvailableReservationStatusException() {
super("예약 취소 가능한 상태가 아닙니다.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.shallwe.domain.reservation.exception;

public class UserReservationMismatchException extends RuntimeException {

public UserReservationMismatchException() {
super("예약자 정보가 일치하지 않습니다.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.time.LocalDate;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand Down Expand Up @@ -148,18 +149,18 @@ public ResponseCustom<ReservationResponse> updateReservation(

}

@Operation(summary = "예약 삭제하기", description = "예약을 삭제합니다")
@Operation(summary = "예약 취소하기", description = "예약을 취소합니다")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "예약 삭제 성공", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = DeleteReservationRes.class))}),
@ApiResponse(responseCode = "400", description = "예약 삭제 실패", content = {
@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))})
@ApiResponse(responseCode = "201", description = "예약 취소 성공"),
@ApiResponse(responseCode = "400", description = "예약 취소 실패", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))})
})
@DeleteMapping
public ResponseCustom<DeleteReservationRes> deleteReservation(
@Parameter(description = "예약 ID를 확인해주세요.", required = true) @RequestHeader Long id
) {
return ResponseCustom.OK(reservationManipulationService.deleteReservation(id));
@PatchMapping("/{reservation-id}")
public ResponseEntity<Void> cancelReservation(
@Parameter(description = "AccessToken 을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(name = "reservationId", description = "예약 ID를 확인해주세요.", required = true) @PathVariable(name = "reservation-id") Long reservationId
) throws Exception {
reservationManipulationService.cancelReservation(userPrincipal, reservationId);
return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface ShopOwnerService {

List<ValidTimeSlotRes> getShopOwnerReservation(UserPrincipal userPrincipal, Long giftId);

Message confirmPayment(UserPrincipal userPrincipal, Long reservationId);
Message confirmPayment(UserPrincipal userPrincipal, Long reservationId) throws Exception;

Message uploadShopOwnerIdentification(ShopOwnerIdentificationReq shopOwnerIdentificationReq, UserPrincipal userPrincipal);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
import com.shallwe.domain.shopowner.domain.repository.ShopOwnerRepository;
import com.shallwe.domain.shopowner.dto.ShopOwnerIdentificationReq;
import com.shallwe.domain.shopowner.exception.InvalidShopOwnerException;
import com.shallwe.domain.shopowner.exception.NotReservedException;
import com.shallwe.domain.shopowner.exception.ShopOwnerExperienceGiftMismatchException;
import com.shallwe.domain.user.exception.InvalidTokenException;
import com.shallwe.global.config.security.token.UserPrincipal;
import com.shallwe.global.infrastructure.sms.NaverSmsClient;
import com.shallwe.global.payload.Message;
import com.shallwe.global.utils.AwsS3ImageUrlUtil;

import java.util.List;
import java.util.Objects;

import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand All @@ -37,76 +41,82 @@
@Transactional(readOnly = true)
public class ShopOwnerServiceImpl implements ShopOwnerService {

private final PasswordEncoder passwordEncoder;
private final ShopOwnerRepository shopOwnerRepository;
private final TokenRepository tokenRepository;
private final ReservationRepository reservationRepository;
private final ExperienceGiftRepository experienceGiftRepository;

@Override
@Transactional
public Message deleteCurrentShopOwner(UserPrincipal userPrincipal) {
ShopOwner shopOwner = shopOwnerRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidShopOwnerException::new);
Token token = tokenRepository.findByUserEmail(userPrincipal.getEmail())
.orElseThrow(InvalidTokenException::new);

shopOwner.updateStatus(Status.DELETE);
tokenRepository.delete(token);

return Message.builder()
.message("사장 탈퇴가 완료되었습니다.")
.build();
}

@Override
public List<ValidTimeSlotRes> getShopOwnerReservation(UserPrincipal userPrincipal,
Long giftId) {
ExperienceGift experienceGift = experienceGiftRepository.findById(giftId)
.orElseThrow(ExperienceGiftNotFoundException::new);
List<Reservation> reservationList = reservationRepository.findAllByExperienceGift(
experienceGift).orElseThrow(InvalidAvailableTimeException::new);

return reservationList.stream().map(reservation -> ValidTimeSlotRes.builder()
.reservationId(reservation.getId())
.date(reservation.getDate())
.time(reservation.getTime())
.build()).toList();
}

@Override
@Transactional
public Message confirmPayment(UserPrincipal userPrincipal, Long reservationId) {
Reservation reservation = reservationRepository.findById(reservationId).orElseThrow(
InvalidReservationException::new);
if (!reservation.getReservationStatus().equals(BOOKED)) {
return Message.builder()
.message("올바르지 않은 시도입니다.")
.build();
} else {
reservation.updateStatus(CONFIRMED);
reservationRepository.save(reservation);
return Message.builder()
.message("예약이 확정되었습니다.")
.build();
private final NaverSmsClient naverSmsClient;
private final ShopOwnerRepository shopOwnerRepository;
private final TokenRepository tokenRepository;
private final ReservationRepository reservationRepository;
private final ExperienceGiftRepository experienceGiftRepository;

@Override
@Transactional
public Message deleteCurrentShopOwner(UserPrincipal userPrincipal) {
ShopOwner shopOwner = shopOwnerRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidShopOwnerException::new);
Token token = tokenRepository.findByUserEmail(userPrincipal.getEmail())
.orElseThrow(InvalidTokenException::new);

shopOwner.updateStatus(Status.DELETE);
tokenRepository.delete(token);

return Message.builder()
.message("사장 탈퇴가 완료되었습니다.")
.build();
}
}

@Override
@Transactional
public Message uploadShopOwnerIdentification(
ShopOwnerIdentificationReq shopOwnerIdentificationReq, UserPrincipal userPrincipal) {
ShopOwner shopOwner = shopOwnerRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidShopOwnerException::new);

shopOwner.updateIdentification(
AwsS3ImageUrlUtil.toUrl(shopOwnerIdentificationReq.getIdentification()));
shopOwner.updateBusinessRegistration(
AwsS3ImageUrlUtil.toUrl(shopOwnerIdentificationReq.getBusinessRegistration()));
shopOwner.updateBankbook(AwsS3ImageUrlUtil.toUrl(shopOwnerIdentificationReq.getBankbook()));

return Message.builder()
.message("사장님 신분증/사업자등록증/통장사본 등록이 완료되었습니다.")
.build();
}

@Override
public List<ValidTimeSlotRes> getShopOwnerReservation(UserPrincipal userPrincipal,
Long giftId) {
ExperienceGift experienceGift = experienceGiftRepository.findById(giftId)
.orElseThrow(ExperienceGiftNotFoundException::new);
List<Reservation> reservationList = reservationRepository.findAllByExperienceGift(
experienceGift).orElseThrow(InvalidAvailableTimeException::new);

return reservationList.stream().map(reservation -> ValidTimeSlotRes.builder()
.reservationId(reservation.getId())
.date(reservation.getDate())
.time(reservation.getTime())
.build()).toList();
}

@Override
@Transactional
public Message confirmPayment(UserPrincipal userPrincipal, Long reservationId) throws Exception {
ShopOwner shopOwner = shopOwnerRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidShopOwnerException::new);

Reservation reservation = reservationRepository.findReservationById(reservationId).orElseThrow(
InvalidReservationException::new);

if(!reservation.getExperienceGift().getShopOwner().getId().equals(shopOwner.getId())) // 경험 선물과 사장님이 일치하지 않을 경우
throw new ShopOwnerExperienceGiftMismatchException();

if (!reservation.getReservationStatus().equals(BOOKED)) // 예약 상태가 BOOKED가 아닐 경우
throw new NotReservedException();

reservation.updateStatus(CONFIRMED);
naverSmsClient.sendInvitationAndConfirm(reservation);

return Message.builder()
.message("예약이 확정되었습니다.")
.build();
}

@Override
@Transactional
public Message uploadShopOwnerIdentification(
ShopOwnerIdentificationReq shopOwnerIdentificationReq, UserPrincipal userPrincipal) {
ShopOwner shopOwner = shopOwnerRepository.findById(userPrincipal.getId())
.orElseThrow(InvalidShopOwnerException::new);

shopOwner.updateIdentification(
AwsS3ImageUrlUtil.toUrl(shopOwnerIdentificationReq.getIdentification()));
shopOwner.updateBusinessRegistration(
AwsS3ImageUrlUtil.toUrl(shopOwnerIdentificationReq.getBusinessRegistration()));
shopOwner.updateBankbook(AwsS3ImageUrlUtil.toUrl(shopOwnerIdentificationReq.getBankbook()));

return Message.builder()
.message("사장님 신분증/사업자등록증/통장사본 등록이 완료되었습니다.")
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.shallwe.domain.shopowner.exception;

public class NotReservedException extends RuntimeException {

public NotReservedException() {
super("예약 확정 대기상태가 아닌 예약입니다.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.shallwe.domain.shopowner.exception;

public class ShopOwnerExperienceGiftMismatchException extends RuntimeException {

public ShopOwnerExperienceGiftMismatchException() {
super("해당 사장의 상품이 아닙니다.");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ public ResponseCustom<List<ReservationIdOwnerRes>> getReservationWithDate(
@PostMapping("/confirm")
public ResponseCustom<Message> confirmReservationPayment(
@Parameter(description = "AccessToken 을 입력해주세요.", required = true) @CurrentUser UserPrincipal userPrincipal,
@Parameter(description = "예약 ID를 입력해주세요", required = true) @RequestParam Long reservationId
) {
@Parameter(name = "reservationId", description = "예약 ID를 입력해주세요", required = true) @RequestParam(name = "reservationId") Long reservationId
) throws Exception {
return ResponseCustom.OK(shopOwnerService.confirmPayment(userPrincipal, reservationId));
}

Expand Down
Loading

0 comments on commit a31611b

Please sign in to comment.