Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#35): Add maintenance windows as recurring bookings #37

Merged
merged 2 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import com.mashreq.security.jwt.TokenService;
import com.mashreq.users.User;
import com.mashreq.users.UserRepository;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -75,8 +75,8 @@ public void signup(SignupPayload payload) {

User userToRegister = payload.toEntity();
userToRegister.setPassword(passwordEncoder.encode(payload.password()));
userToRegister.setCreatedOn(Instant.now());
userToRegister.setModifiedOn(Instant.now());
userToRegister.setCreatedOn(LocalDateTime.now());
userToRegister.setModifiedOn(LocalDateTime.now());

userRepository.save(userToRegister);
}
Expand Down Expand Up @@ -104,7 +104,7 @@ public LoginResult login(String username, String password) {
log.debug("Successfully logged in user");

// update last login date
user.setLastLoggedInOn(Instant.now());
user.setLastLoggedInOn(LocalDateTime.now());
userRepository.save(user);

return new LoginResult(user, token);
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/com/mashreq/bookings/AbstractBooking.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.mashreq.bookings;

import com.mashreq.common.models.BaseEntity;
import com.mashreq.rooms.Room;
import com.mashreq.users.User;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
@MappedSuperclass
public abstract class AbstractBooking extends BaseEntity {
@ManyToOne
@JoinColumn(name = "room_id", nullable = false)
private Room room;

@ManyToOne
@JoinColumn(name = "user_id", nullable = true)
private User user;

@Column
private String name;

@Column
private String description;

@Enumerated(EnumType.STRING)
@Column
private BookingStatus status = BookingStatus.BOOKED;
}
17 changes: 4 additions & 13 deletions src/main/java/com/mashreq/bookings/Booking.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import jakarta.persistence.Enumerated;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.time.Instant;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
Expand All @@ -28,23 +28,14 @@
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "bookings")
public class Booking extends BaseEntity {

@ManyToOne
private User user;

@ManyToOne
private Room room;
public class Booking extends AbstractBooking {

@Column(name = "number_of_people", nullable = false)
private int numberOfPeople;

@Column(name = "start_time", nullable = false)
private Instant startTime;
private LocalDateTime startTime;

@Column(name = "end_time", nullable = false)
private Instant endTime;

@Enumerated(EnumType.STRING)
private BookingStatus status = BookingStatus.BOOKED;
private LocalDateTime endTime;
}
99 changes: 81 additions & 18 deletions src/main/java/com/mashreq/bookings/BookingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,104 @@

import com.mashreq.bookings.payload.BookingRequest;
import com.mashreq.bookings.results.BookingResult;
import com.mashreq.common.TimeUtils;
import com.mashreq.common.exceptions.MaintenanceInProgressException;
import com.mashreq.common.exceptions.NoRoomsAvailableException;
import com.mashreq.rooms.Room;
import com.mashreq.rooms.RoomService;
import com.mashreq.security.AuthenticatedUser;
import com.mashreq.users.User;
import com.mashreq.users.UserService;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Service class for handling booking operations.
*/
@Slf4j
@Service
public class BookingService {
private BookingRepository bookingRepository;
private RoomService roomService;
private UserService userService;

private final RecurringBookingRepository recurringBookingRepository;
private final BookingRepository bookingRepository;
private final RoomService roomService;
private final UserService userService;

/**
* Constructs a BookingService with the required repositories and services.
*
* @param recurringBookingRepository the repository for recurring bookings
* @param bookingRepository the repository for bookings
* @param roomService the service for room operations
* @param userService the service for user operations
*/
public BookingService(
BookingRepository bookingRepository, RoomService roomService, UserService userService) {
RecurringBookingRepository recurringBookingRepository,
BookingRepository bookingRepository,
RoomService roomService,
UserService userService) {
this.recurringBookingRepository = recurringBookingRepository;
this.bookingRepository = bookingRepository;
this.roomService = roomService;
this.userService = userService;
}

/**
* Creates a booking based on the provided request and user.
*
* @param authenticatedUser the authenticated user making the booking
* @param payload the details of the booking request
* @return a result representing the outcome of the booking creation
* @throws NoRoomsAvailableException if no rooms are available for the requested time and capacity
*/
@Transactional
public BookingResult createBooking(AuthenticatedUser authenticatedUser, BookingRequest payload) {
User user = userService.getUserByUsername(authenticatedUser.getUsername());
// Get the available rooms in the timeframe with enough capacity
Room room = roomService.getAvailableRoomWithOptimumCapacity(payload.startTime(), payload.endTime(), payload.numberOfPeople());

Booking booking = Booking.builder()
.room(room)
.user(user)
.startTime(payload.startTime())
.endTime(payload.endTime())
.numberOfPeople(payload.numberOfPeople())
.build();
bookingRepository.save(booking);
return BookingResult.toResult(booking);
public BookingResult createBooking(AuthenticatedUser authenticatedUser, BookingRequest payload)
throws NoRoomsAvailableException {
try {
User user = userService.getUserByUsername(authenticatedUser.getUsername());
Room room = roomService.getAvailableRoomWithOptimumCapacity(payload.startTime(), payload.endTime(), payload.numberOfPeople());

Booking booking = BookingRequest.toBooking(payload, user, room);
bookingRepository.save(booking);

return BookingResult.toResult(booking);

} catch (NoRoomsAvailableException e) {
log.warn("No available rooms found for the given time and capacity. Checking recurring bookings...");
checkRecurringMaintenanceBooking(payload.startTime(), payload.endTime(), payload.numberOfPeople());
throw e; // Re-throw to signal failure
}
}

/**
* Checks for recurring maintenance bookings that might overlap with the given time range.
*
* @param startTime the start time of the booking request
* @param endTime the end time of the booking request
* @param numberOfPeople the number of people for the booking
* @throws MaintenanceInProgressException if there is an ongoing maintenance booking that overlaps with the request
*/
private void checkRecurringMaintenanceBooking(
LocalDateTime startTime, LocalDateTime endTime, int numberOfPeople)
throws MaintenanceInProgressException {
log.info("Checking for recurring maintenance bookings for the given time range...");

// Convert LocalDateTime to LocalTime using system default time zone
LocalTime startLocalTime = TimeUtils.convertLocalDateTimeToLocalTime(startTime);
LocalTime endLocalTime = TimeUtils.convertLocalDateTimeToLocalTime(endTime);

List<RecurringBooking> recurringBookings = recurringBookingRepository.findRecurringMaintenanceBooking(
startLocalTime, endLocalTime, numberOfPeople
);

if (!recurringBookings.isEmpty()) {
// If recurring maintenance found, throw an exception
throw new MaintenanceInProgressException();
}
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/mashreq/bookings/BookingType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mashreq.bookings;

public enum BookingType {
MAINTENANCE, RECURRING_MEETING, MEETING
}
40 changes: 40 additions & 0 deletions src/main/java/com/mashreq/bookings/RecurringBooking.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.mashreq.bookings;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Table;
import java.sql.Time;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
* Representation of the Room entity in the system.
*/
@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "recurring_bookings")
public class RecurringBooking extends AbstractBooking {

@Column(name = "start_time")
private Time startTime;

@Column(name = "end_time")
private Time endTime;

@Column(name = "booking_type")
@Enumerated(EnumType.STRING)
private BookingType bookingType = BookingType.MAINTENANCE;


}
27 changes: 27 additions & 0 deletions src/main/java/com/mashreq/bookings/RecurringBookingRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mashreq.bookings;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface RecurringBookingRepository extends JpaRepository<RecurringBooking, UUID> {

@Query(value = """
SELECT rb.*
FROM recurring_bookings rb
INNER JOIN rooms r ON r.id = rb.room_id
WHERE rb.booking_type = 'MAINTENANCE'
AND r.capacity >= :numberOfPeople
AND rb.start_time < :endTime
AND rb.end_time > :startTime
""", nativeQuery = true)
List<RecurringBooking> findRecurringMaintenanceBooking(
@Param("startTime") LocalTime startTime,
@Param("endTime") LocalTime endTime,
@Param("numberOfPeople") int numberOfPeople);

}
32 changes: 27 additions & 5 deletions src/main/java/com/mashreq/bookings/payload/BookingRequest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.mashreq.bookings.payload;

import com.mashreq.bookings.Booking;
import com.mashreq.bookings.validators.IsMultipleOf15Minutes;
import com.mashreq.bookings.validators.IsToday;
import com.mashreq.bookings.validators.ValidNumberOfPeople;
import com.mashreq.bookings.validators.ValidTimeRange;
import com.mashreq.rooms.Room;
import com.mashreq.users.User;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

/**
* POJO for creating a Booking.
Expand All @@ -15,16 +19,34 @@ public record BookingRequest(
@NotNull
@IsToday(message = "{error.startTime.today}")
@IsMultipleOf15Minutes(message = "{error.startTime.15mins}")
Instant startTime,
LocalDateTime startTime,

@NotNull
@IsToday(message = "{error.endTime.today}")
@IsMultipleOf15Minutes(message = "{error.endTime.15mins}")
Instant endTime,
LocalDateTime endTime,

@ValidNumberOfPeople
@NotNull
int numberOfPeople
) {
int numberOfPeople,

@Size(max = 100, message = "{error.booking.nameLength}")
@NotNull(message = "{error.booking.nameNull}")
String name,


@Size(max = 255, message = "{error.booking.descriptionLength}")
@NotNull(message = "{error.booking.descriptionNull}")
String description
) {
public static Booking toBooking(BookingRequest payload, User user, Room room) {
Booking booking = Booking.builder()
.startTime(payload.startTime)
.endTime(payload.endTime)
.numberOfPeople(payload.numberOfPeople)
.build();
booking.setRoom(room);
booking.setUser(user);
return booking;
}
}
18 changes: 11 additions & 7 deletions src/main/java/com/mashreq/bookings/results/BookingResult.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
package com.mashreq.bookings.results;

import com.mashreq.bookings.BookingType;
import com.mashreq.bookings.Booking;
import com.mashreq.rooms.Room;
import com.mashreq.users.User;
import java.time.Instant;
import java.time.LocalDateTime;

public record BookingResult(
Instant startTime,
Instant endTime,
String name,
String description,
LocalDateTime startTime,
LocalDateTime endTime,
User user,
Room room,
int numberOfPeople
int numberOfPeople,
BookingType bookingType
) {
public static BookingResult toResult(Booking booking) {
// Extract fields from the Booking object
Instant startTime = booking.getStartTime();
Instant endTime = booking.getEndTime();
LocalDateTime startTime = booking.getStartTime();
LocalDateTime endTime = booking.getEndTime();
User user = booking.getUser();
Room room = booking.getRoom();
int numberOfPeople = booking.getNumberOfPeople();

// Create and return a new BookingResult instance
return new BookingResult(startTime, endTime, user, room, numberOfPeople);
return new BookingResult(booking.getName(), booking.getDescription(), startTime, endTime, user, room, numberOfPeople, BookingType.MEETING);
}
}
Loading
Loading