package com.paymentlink.reservation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.concurrent.ConcurrentHashMap; @Service public class ReservationService { private static final Logger logger = LoggerFactory.getLogger(ReservationService.class); @Value("${reservation.timeout.ms:90000}") private long reservationTimeout; // Map: sessionId -> (productId -> Reservation) private final ConcurrentHashMap> reservations = new ConcurrentHashMap<>(); /** * Reserve stock for a user's cart */ public synchronized void reserve(String sessionId, Long productId, int quantity, int totalStock) { logger.debug("Attempting to reserve {} units of product {} for session {}", quantity, productId, sessionId); if (quantity <= 0) { throw new IllegalArgumentException("Quantity must be positive"); } // Calculate how much is reserved by others int reservedByOthers = getReservedCountExcludingSession(productId, sessionId); int availableForUser = totalStock - reservedByOthers; logger.debug("Product {}: total stock = {}, reserved by others = {}, available = {}", productId, totalStock, reservedByOthers, availableForUser); if (availableForUser < quantity) { throw new IllegalStateException(String.format( "Insufficient stock. Only %d items available (you are trying to add %d)", availableForUser, quantity )); } // Get or create session map ConcurrentHashMap sessionMap = reservations.computeIfAbsent( sessionId, k -> new ConcurrentHashMap<>() ); // Create or update reservation Reservation reservation = new Reservation( sessionId, productId, quantity, System.currentTimeMillis() ); sessionMap.put(productId, reservation); logger.info("Reserved {} units of product {} for session {}", quantity, productId, sessionId); } /** * Release a reservation */ public synchronized void release(String sessionId, Long productId) { ConcurrentHashMap sessionMap = reservations.get(sessionId); if (sessionMap != null) { Reservation removed = sessionMap.remove(productId); if (removed != null) { logger.info("Released reservation for product {} from session {}", productId, sessionId); } // Clean up empty session map if (sessionMap.isEmpty()) { reservations.remove(sessionId); } } } /** * Get total reserved count for a product (all sessions) */ public int getReservedCount(Long productId) { int total = 0; for (ConcurrentHashMap sessionMap : reservations.values()) { Reservation reservation = sessionMap.get(productId); if (reservation != null && !reservation.isExpired(reservationTimeout)) { total += reservation.getQuantity(); } } return total; } /** * Get reserved count excluding a specific session */ public int getReservedCountExcludingSession(Long productId, String excludeSessionId) { int total = 0; for (String sessionId : reservations.keySet()) { if (!sessionId.equals(excludeSessionId)) { ConcurrentHashMap sessionMap = reservations.get(sessionId); if (sessionMap != null) { Reservation reservation = sessionMap.get(productId); if (reservation != null && !reservation.isExpired(reservationTimeout)) { total += reservation.getQuantity(); } } } } return total; } /** * Get reservation for a specific session and product */ public Integer getSessionReservation(String sessionId, Long productId) { ConcurrentHashMap sessionMap = reservations.get(sessionId); if (sessionMap != null) { Reservation reservation = sessionMap.get(productId); if (reservation != null && !reservation.isExpired(reservationTimeout)) { return reservation.getQuantity(); } } return 0; } /** * Clean up expired reservations */ public synchronized void cleanup() { final java.util.concurrent.atomic.AtomicInteger cleanedCount = new java.util.concurrent.atomic.AtomicInteger(0); for (String sessionId : reservations.keySet()) { ConcurrentHashMap sessionMap = reservations.get(sessionId); if (sessionMap != null) { sessionMap.entrySet().removeIf(entry -> { boolean expired = entry.getValue().isExpired(reservationTimeout); if (expired) { cleanedCount.incrementAndGet(); logger.debug("Removing expired reservation for product {} from session {}", entry.getKey(), sessionId); } return expired; }); // Clean up empty session map if (sessionMap.isEmpty()) { reservations.remove(sessionId); } } } if (cleanedCount.get() > 0) { logger.info("Cleaned up {} expired reservations", cleanedCount.get()); } } /** * Get current timeout value (for testing/debugging) */ public long getReservationTimeout() { return reservationTimeout; } }