diff --git a/src/main/java/com/slotline/reservation/WaitlistService.java b/src/main/java/com/slotline/reservation/WaitlistService.java new file mode 100644 index 0000000..67412b5 --- /dev/null +++ b/src/main/java/com/slotline/reservation/WaitlistService.java @@ -0,0 +1,80 @@ +package com.slotline.reservation; + +import com.slotline.common.InvalidReservationException; +import com.slotline.notification.NotificationMessage; +import com.slotline.notification.NotificationService; +import com.slotline.notification.NotificationType; +import com.slotline.reservation.dto.WaitlistRequest; +import com.slotline.reservation.dto.WaitlistResponse; +import com.slotline.resource.Resource; +import com.slotline.resource.ResourceService; +import com.slotline.security.AuthenticatedUser; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.List; + +@Service +public class WaitlistService { + + private final WaitlistRepository waitlist; + private final ReservationRepository reservations; + private final ResourceService resources; + private final NotificationService notifications; + + public WaitlistService(WaitlistRepository waitlist, ReservationRepository reservations, ResourceService resources, + NotificationService notifications) { + this.waitlist = waitlist; + this.reservations = reservations; + this.resources = resources; + this.notifications = notifications; + } + + @Transactional + public WaitlistResponse join(AuthenticatedUser actor, WaitlistRequest request) { + if (!request.startTime().isBefore(request.endTime())) { + throw new InvalidReservationException("startTime must be strictly before endTime"); + } + Resource resource = resources.require(actor.organizationId(), request.resourceId()); + WaitlistEntry entry = new WaitlistEntry(resource.getId(), resource.getOrganizationId(), actor.id(), + request.startTime(), request.endTime(), Instant.now()); + return WaitlistResponse.from(waitlist.save(entry)); + } + + @Transactional(readOnly = true) + public List listForUser(Long userId) { + return waitlist.findByUserIdOrderByCreatedAtDesc(userId).stream() + .map(WaitlistResponse::from) + .toList(); + } + + @Transactional + public void promoteAfterCancellation(Reservation cancelled) { + Resource resource = cancelled.getResource(); + List candidates = + waitlist.findWaitingOverlaps(resource.getId(), cancelled.getStartTime(), cancelled.getEndTime()); + for (WaitlistEntry candidate : candidates) { + boolean stillFree = reservations + .findActiveOverlaps(resource.getId(), candidate.getStartTime(), candidate.getEndTime()) + .isEmpty(); + if (!stillFree) { + continue; + } + Reservation promoted = new Reservation(resource, candidate.getUserId(), candidate.getStartTime(), + candidate.getEndTime(), Instant.now()); + reservations.save(promoted); + candidate.promote(promoted.getId()); + notifications.dispatch(new NotificationMessage( + NotificationType.WAITLIST_PROMOTED, + resource.getOrganizationId(), + candidate.getUserId(), + resource.getId(), + resource.getName(), + promoted.getStartTime(), + promoted.getEndTime(), + "A waitlisted slot became available and was booked for you")); + return; + } + } +} diff --git a/src/main/java/com/slotline/reservation/dto/WaitlistRequest.java b/src/main/java/com/slotline/reservation/dto/WaitlistRequest.java new file mode 100644 index 0000000..aea482a --- /dev/null +++ b/src/main/java/com/slotline/reservation/dto/WaitlistRequest.java @@ -0,0 +1,11 @@ +package com.slotline.reservation.dto; + +import jakarta.validation.constraints.NotNull; + +import java.time.Instant; + +public record WaitlistRequest( + @NotNull Long resourceId, + @NotNull Instant startTime, + @NotNull Instant endTime) { +} diff --git a/src/main/java/com/slotline/reservation/dto/WaitlistResponse.java b/src/main/java/com/slotline/reservation/dto/WaitlistResponse.java new file mode 100644 index 0000000..73fc931 --- /dev/null +++ b/src/main/java/com/slotline/reservation/dto/WaitlistResponse.java @@ -0,0 +1,29 @@ +package com.slotline.reservation.dto; + +import com.slotline.reservation.WaitlistEntry; +import com.slotline.reservation.WaitlistStatus; + +import java.time.Instant; + +public record WaitlistResponse( + Long id, + Long resourceId, + Long userId, + Instant startTime, + Instant endTime, + WaitlistStatus status, + Long promotedReservationId, + Instant createdAt) { + + public static WaitlistResponse from(WaitlistEntry entry) { + return new WaitlistResponse( + entry.getId(), + entry.getResourceId(), + entry.getUserId(), + entry.getStartTime(), + entry.getEndTime(), + entry.getStatus(), + entry.getPromotedReservationId(), + entry.getCreatedAt()); + } +}