diff --git a/src/main/java/com/application/bookingservice/bot/NotificationBot.java b/src/main/java/com/application/bookingservice/bot/NotificationBot.java index f2a559f..47c67cb 100644 --- a/src/main/java/com/application/bookingservice/bot/NotificationBot.java +++ b/src/main/java/com/application/bookingservice/bot/NotificationBot.java @@ -1,10 +1,20 @@ package com.application.bookingservice.bot; +import com.application.bookingservice.dto.customer.CustomerLoginRequestDto; import com.application.bookingservice.exception.TelegramMessageException; +import com.application.bookingservice.model.Customer; +import com.application.bookingservice.repository.customer.CustomerRepository; +import jakarta.persistence.EntityNotFoundException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.stereotype.Component; import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; @@ -12,17 +22,36 @@ public class NotificationBot extends TelegramLongPollingBot { private static final Long CHAT_ID = -1002107651145L; private static final String START_COMMAND = "/start"; - private static final String CANT_HANDLE_START_MESSAGE = "Can't handle start command"; + private static final String AUTH_COMMAND = "/auth"; + private static final String CANCEL_COMMAND = "/cancel"; private static final String CANT_LOG_MESSAGE = "Can't send logs to chat text: %s"; private static final String CANT_SEND_MESSAGE = "Can't send message to user chatId: %d"; - private static final String START_MESSAGE = "Hello, I am a bot for logging in BINOV booking"; + private static final String START_MESSAGE = "hello im BINOV_booking_bot use /auth command " + + "if you haven't done it yet to receive notification " + + "or /cancel to stop receive messages"; + private static final String DELETE_MESSAGE = "Can't delete user message"; private final String botName; + private final CustomerRepository customerRepository; + private CustomerLoginRequestDto requestDto; + private final AuthenticationManager authenticationManager; + private Map authStates = new HashMap<>(); + private Map loginData = new HashMap<>(); + + private enum AuthState { + STARTED, + LOGIN, + PASSWORD + } public NotificationBot( @Value("${bot.token}") String token, - @Value("${bot.name}") String botName + @Value("${bot.name}") String botName, + CustomerRepository customerRepository, + AuthenticationManager authenticationManager ) { super(token); + this.customerRepository = customerRepository; + this.authenticationManager = authenticationManager; this.botName = botName; } @@ -33,12 +62,87 @@ public String getBotUsername() { @Override public void onUpdateReceived(Update update) { + Long chatId = update.getMessage().getChatId(); + AuthState currentState = authStates.getOrDefault(chatId, AuthState.STARTED); + String recivedText = update.getMessage().getText(); + if (recivedText.equals(START_COMMAND)) { + handleStartCommand(update); + } else if (authStates.containsKey(chatId)) { + authProcess(currentState, chatId, update); + } else if (recivedText.equals(AUTH_COMMAND)) { + sendTextMessage(chatId, "Please enter your login:"); + authStates.put(chatId, AuthState.LOGIN); + } else if (recivedText.equals(CANCEL_COMMAND)) { + Customer customer = customerRepository.findByChatId(chatId).orElseThrow( + () -> new EntityNotFoundException("can't find customer by chat id") + ); + customer.setChatId(null); + customerRepository.save(customer); + sendTextMessage(chatId, "notification canceled successful"); + } else { + sendTextMessage(chatId, START_MESSAGE); + } + } + + private void authProcess(AuthState currentState, Long chatId, Update update) { + String text = update.getMessage().getText(); + switch (currentState) { + case STARTED: + sendTextMessage(chatId, "Please enter your login:"); + authStates.put(chatId, AuthState.LOGIN); + break; + case LOGIN: + requestDto = loginData.getOrDefault(chatId, new CustomerLoginRequestDto()); + requestDto.setEmail(text); + sendTextMessage(chatId, "Password:"); + loginData.put(chatId, requestDto); + authStates.put(chatId, AuthState.PASSWORD); + break; + case PASSWORD: + requestDto = loginData.getOrDefault(chatId, new CustomerLoginRequestDto()); + requestDto.setPassword(text); + deletePreviousMessage(chatId, update); + try { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( + requestDto.getEmail(), requestDto.getPassword() + )); + Customer customer = customerRepository.findByEmail( + requestDto.getEmail()).orElseThrow( + () -> new EntityNotFoundException("Can't find customer") + ); + customer.setChatId(chatId); + customerRepository.save(customer); + sendTextMessage(chatId, "Successful, now you will receive " + + "notifications from BINOV_booking"); + } catch (Exception e) { + sendTextMessage(chatId, "Wrong password or login please try again"); + } finally { + authStates.remove(chatId); + requestDto.setPassword(""); + } + break; + default: + authStates.put(chatId, AuthState.STARTED); + break; + } + } + + public void sendTextMessageToAll(String text) { + List allByChatIdNotNull = customerRepository.findAllByChatIdNotNull(); + for (int i = 0; i < allByChatIdNotNull.size(); i++) { + sendTextMessage(allByChatIdNotNull.get(i).getChatId(), text); + } + } + + public void sendTextMessage(long chatId, String text) { + SendMessage message = new SendMessage(); + message.setChatId(chatId); + message.setText(text); + try { - if (update.getMessage().getText().equals(START_COMMAND)) { - handleStartCommand(update); - } - } catch (Exception e) { - throw new TelegramMessageException(CANT_HANDLE_START_MESSAGE, e); + execute(message); + } catch (TelegramApiException e) { + throw new RuntimeException(); } } @@ -65,4 +169,16 @@ private void handleStartCommand(Update update) { } } + private void deletePreviousMessage(Long chatId, Update update) { + int messageId = update.getMessage().getMessageId(); + DeleteMessage deleteMessage = new DeleteMessage(); + deleteMessage.setChatId(chatId); + deleteMessage.setMessageId(messageId); + try { + execute(deleteMessage); + } catch (TelegramApiException e) { + throw new TelegramMessageException(DELETE_MESSAGE, e); + } + } + } diff --git a/src/main/java/com/application/bookingservice/controller/BotController.java b/src/main/java/com/application/bookingservice/controller/BotController.java new file mode 100644 index 0000000..52107df --- /dev/null +++ b/src/main/java/com/application/bookingservice/controller/BotController.java @@ -0,0 +1,31 @@ +package com.application.bookingservice.controller; + +import com.application.bookingservice.dto.bot.BotRequestDto; +import com.application.bookingservice.service.bot.NotificationService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/telegram") +public class BotController { + private final NotificationService notificationService; + + @PreAuthorize("hasRole('ROLE_MANAGER')") + @PostMapping + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Send notification", + description = "Permits to send message to all users") + public void sendNotification(@RequestBody @Valid BotRequestDto botRequestDto) { + notificationService.sendToAllUsers(botRequestDto.getMessage()); + } + +} diff --git a/src/main/java/com/application/bookingservice/dto/bot/BotRequestDto.java b/src/main/java/com/application/bookingservice/dto/bot/BotRequestDto.java new file mode 100644 index 0000000..8dd9637 --- /dev/null +++ b/src/main/java/com/application/bookingservice/dto/bot/BotRequestDto.java @@ -0,0 +1,10 @@ +package com.application.bookingservice.dto.bot; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class BotRequestDto { + @NotBlank + private String message; +} diff --git a/src/main/java/com/application/bookingservice/model/Customer.java b/src/main/java/com/application/bookingservice/model/Customer.java index 5cc13f8..5e3b2db 100644 --- a/src/main/java/com/application/bookingservice/model/Customer.java +++ b/src/main/java/com/application/bookingservice/model/Customer.java @@ -47,6 +47,8 @@ public class Customer implements UserDetails { inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set roles = new HashSet<>(); + @Column(name = "chat_id") + private Long chatId; @Column(name = "is_deleted", nullable = false) private Boolean isDeleted = false; diff --git a/src/main/java/com/application/bookingservice/repository/customer/CustomerRepository.java b/src/main/java/com/application/bookingservice/repository/customer/CustomerRepository.java index 4032311..1411156 100644 --- a/src/main/java/com/application/bookingservice/repository/customer/CustomerRepository.java +++ b/src/main/java/com/application/bookingservice/repository/customer/CustomerRepository.java @@ -1,6 +1,7 @@ package com.application.bookingservice.repository.customer; import com.application.bookingservice.model.Customer; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -13,4 +14,8 @@ public interface CustomerRepository extends JpaRepository { @Query("SELECT c FROM Customer c JOIN FETCH c.roles r WHERE c.id = :id") Optional findById(Long id); + + Optional findByChatId(Long chatId); + + List findAllByChatIdNotNull(); } diff --git a/src/main/java/com/application/bookingservice/service/accommodation/AccommodationServiceImpl.java b/src/main/java/com/application/bookingservice/service/accommodation/AccommodationServiceImpl.java index f19c230..c217c33 100644 --- a/src/main/java/com/application/bookingservice/service/accommodation/AccommodationServiceImpl.java +++ b/src/main/java/com/application/bookingservice/service/accommodation/AccommodationServiceImpl.java @@ -39,6 +39,7 @@ public AccommodationResponseDto save(AccommodationRequestDto accommodationReques AccommodationResponseDto savedAccommodationDto = accommodationMapper.toDto(accommodationRepository.save(accommodation)); + notificationService.sendToUserNewAccommodation(savedAccommodationDto); notificationService.accommodationCreatedMessage(savedAccommodationDto); return savedAccommodationDto; } diff --git a/src/main/java/com/application/bookingservice/service/booking/BookingServiceImpl.java b/src/main/java/com/application/bookingservice/service/booking/BookingServiceImpl.java index 3366674..fbeecf8 100644 --- a/src/main/java/com/application/bookingservice/service/booking/BookingServiceImpl.java +++ b/src/main/java/com/application/bookingservice/service/booking/BookingServiceImpl.java @@ -12,6 +12,7 @@ import com.application.bookingservice.exception.UnauthorizedActionException; import com.application.bookingservice.mapper.BookingMapper; import com.application.bookingservice.model.Booking; +import com.application.bookingservice.model.Customer; import com.application.bookingservice.repository.booking.BookingRepository; import com.application.bookingservice.repository.booking.spec.BookingSpecificationBuilder; import com.application.bookingservice.repository.customer.CustomerRepository; @@ -81,6 +82,12 @@ public BookingResponseDto save(Long customerId, BookingRequestDto requestBooking )); BookingResponseDto savedBookingDto = bookingMapper .toDto(bookingRepository.save(bookingToSave)); + Customer customer = customerRepository.findById(customerId).orElseThrow( + () -> new EntityNotFoundException(CUSTOMER_NOT_FOUND_MESSAGE) + ); + if (customer.getChatId() != null) { + notificationService.sendToUserBookingSuccessful(customer.getChatId(), savedBookingDto); + } notificationService.bookingsCreatedMessage(savedBookingDto); return savedBookingDto; } @@ -167,8 +174,8 @@ private void checkBookingDate() { LocalDate nowDate = LocalDate.now(); List bookings = bookingRepository.findAll(); for (Booking booking : bookings) { - if (booking.getCheckOut().isAfter(nowDate) - || booking.getCheckOut().isEqual(nowDate)) { + if ((booking.getCheckOut().isAfter(nowDate) + || booking.getCheckOut().isEqual(nowDate)) && booking.getStatus() != EXPIRED) { booking.setStatus(EXPIRED); bookingRepository.save(booking); expired = true; diff --git a/src/main/java/com/application/bookingservice/service/bot/NotificationService.java b/src/main/java/com/application/bookingservice/service/bot/NotificationService.java index 2bcfd99..a48687d 100644 --- a/src/main/java/com/application/bookingservice/service/bot/NotificationService.java +++ b/src/main/java/com/application/bookingservice/service/bot/NotificationService.java @@ -20,4 +20,12 @@ public interface NotificationService { Boolean paymentMessage(Payment payment); Boolean bookingExpiredMessage(String text); + + Boolean sendToUserBookingSuccessful(Long chatId, BookingResponseDto responseDto); + + Boolean sendToUserPayment(Long chatId, Payment payment); + + Boolean sendToUserNewAccommodation(AccommodationResponseDto responseDto); + + Boolean sendToAllUsers(String text); } diff --git a/src/main/java/com/application/bookingservice/service/bot/TelegramNotificationService.java b/src/main/java/com/application/bookingservice/service/bot/TelegramNotificationService.java index db8000d..3470830 100644 --- a/src/main/java/com/application/bookingservice/service/bot/TelegramNotificationService.java +++ b/src/main/java/com/application/bookingservice/service/bot/TelegramNotificationService.java @@ -153,4 +153,47 @@ public Boolean bookingExpiredMessage(String text) { return true; } + @Override + public Boolean sendToUserBookingSuccessful(Long chatId, BookingResponseDto responseDto) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Dear user your booking was successful \n you check in data: ") + .append(responseDto.getCheckIn()) + .append("\nyour check out date :") + .append(responseDto.getCheckOut()) + .append("\nThanks for picking BINOVO_booking!"); + notificationBot.sendTextMessage(chatId, stringBuilder.toString()); + return true; + } + + @Override + public Boolean sendToUserPayment(Long chatId, Payment payment) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(" Dear user your payment was ") + .append(payment.getStatus()) + .append("\ntotal :") + .append(payment.getTotal()) + .append("\nThanks for picking BINOVO_booking!"); + notificationBot.sendTextMessage(chatId, stringBuilder.toString()); + return true; + } + + @Override + public Boolean sendToUserNewAccommodation(AccommodationResponseDto responseDto) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Dear users we added new accommodation \naddress: ") + .append(responseDto.getAddress()) + .append("\ntype: ") + .append(responseDto.getType()) + .append("\nbest price :") + .append(responseDto.getPrice()); + notificationBot.sendTextMessageToAll(stringBuilder.toString()); + return true; + } + + @Override + public Boolean sendToAllUsers(String text) { + notificationBot.sendTextMessageToAll(text); + return true; + } + } diff --git a/src/main/java/com/application/bookingservice/service/payment/PaymentServiceImpl.java b/src/main/java/com/application/bookingservice/service/payment/PaymentServiceImpl.java index 0779e8d..83e5677 100644 --- a/src/main/java/com/application/bookingservice/service/payment/PaymentServiceImpl.java +++ b/src/main/java/com/application/bookingservice/service/payment/PaymentServiceImpl.java @@ -6,6 +6,7 @@ import com.application.bookingservice.exception.EntityNotFoundException; import com.application.bookingservice.mapper.PaymentMapper; import com.application.bookingservice.model.Booking; +import com.application.bookingservice.model.Customer; import com.application.bookingservice.model.Payment; import com.application.bookingservice.repository.booking.BookingRepository; import com.application.bookingservice.repository.payment.PaymentRepository; @@ -44,7 +45,10 @@ public void succeed(String sessionId) { Booking booking = payment.getBooking(); booking.setStatus(Booking.Status.CONFIRMED); bookingRepository.save(booking); - + Customer customer = booking.getCustomer(); + if (customer.getChatId() != null) { + notificationService.sendToUserPayment(customer.getChatId(), payment); + } notificationService.paymentMessage(payment); } @@ -60,7 +64,10 @@ public void cancel(String sessionId) { Booking booking = payment.getBooking(); booking.setStatus(Booking.Status.REJECTED); bookingRepository.save(booking); - + Customer customer = booking.getCustomer(); + if (customer.getChatId() != null) { + notificationService.sendToUserPayment(customer.getChatId(), payment); + } notificationService.paymentMessage(payment); } } diff --git a/src/main/resources/db/changelog/changes/11-add-column-customer-table.yaml b/src/main/resources/db/changelog/changes/11-add-column-customer-table.yaml new file mode 100644 index 0000000..f34a9ef --- /dev/null +++ b/src/main/resources/db/changelog/changes/11-add-column-customer-table.yaml @@ -0,0 +1,11 @@ +databaseChangeLog: + - changeSet: + id: add-column-customer-table + author: BINOV_team + changes: + - addColumn: + tableName: customers + columns: + - column: + name: chat_id + type: bigint diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 1e82b96..1a56bb9 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -19,3 +19,5 @@ databaseChangeLog: file: db/changelog/changes/09-insert-customers.yaml - include: file: db/changelog/changes/10-insert-demo-customers-roles.yaml + - include: + file: db/changelog/changes/11-add-column-customer-table.yaml