feat: implement waitlist promotion service

This commit is contained in:
dualfroz committed 2026-08-26 23:08:58 +02:00
1 parent 84dabf7252
commit 959b400f5a
3 files changed
+120

No files matched your search

@@ -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<WaitlistResponse> listForUser(Long userId) {
return waitlist.findByUserIdOrderByCreatedAtDesc(userId).stream()
.map(WaitlistResponse::from)
.toList();
}
@Transactional
public void promoteAfterCancellation(Reservation cancelled) {
Resource resource = cancelled.getResource();
List<WaitlistEntry> 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;
}
}
}
@@ -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) {
}
@@ -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());
}
}