Add Flash Sale feature for limited-time deals

Problem Context

Product wants to drive engagement with flash sales - limited-time deals with special pricing. Requirements: - Admins can create flash sales with start/end times, discounted prices, and limited quantities - Users can purchase items during active sales (one per user per sale) - System sends email notifications when sales start - Sales automatically activate/deactivate based on schedule - Dashboard shows sale statistics This PR adds the complete flash sale feature across multiple files.

Expert
300 points

File Changes (8)

src/main/java/com/example/entity/FlashSale.java ADDED
@@ -0 +1 @@
1 +package com.example.entity;
2 +
3 +import jakarta.persistence.*;
4 +import lombok.Getter;
5 +import lombok.Setter;
6 +import java.time.LocalDateTime;
7 +import java.util.Objects;
8 +
9 +@Entity
10 +@Table(name = "flash_sales")
11 +@Getter @Setter
12 +public class FlashSale {
13 +
14 + @Id
15 + @GeneratedValue(strategy = GenerationType.IDENTITY)
16 + private Long id;
17 +
18 + @ManyToOne(fetch = FetchType.LAZY)
19 + private Product product;
20 +
21 + private String name;
22 + private Double originalPrice;
23 + private Double discountedPrice;
24 + private Integer availableQuantity;
25 + private Integer soldQuantity = 0;
26 +
27 + private LocalDateTime startTime;
28 + private LocalDateTime endTime;
29 +
30 + @Enumerated(EnumType.STRING)
31 + private SaleStatus status = SaleStatus.SCHEDULED;
32 +
33 + public boolean isActive() {
34 + LocalDateTime now = LocalDateTime.now();
35 + return status == SaleStatus.ACTIVE &&
36 + now.isAfter(startTime) && now.isBefore(endTime);
37 + }
38 +
39 + public int getRemainingStock() {
40 + return availableQuantity - soldQuantity;
41 + }
42 +
43 + @Override
44 + public boolean equals(Object o) {
45 + if (this == o) return true;
46 + if (o == null || getClass() != o.getClass()) return false;
47 + FlashSale that = (FlashSale) o;
48 + return Objects.equals(name, that.name) &&
49 + Objects.equals(discountedPrice, that.discountedPrice) &&
50 + Objects.equals(startTime, that.startTime);
51 + }
52 +
53 + @Override
54 + public int hashCode() {
55 + return Objects.hash(name, discountedPrice, startTime);
56 + }
57 +}
src/main/java/com/example/service/FlashSaleService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.FlashSale;
4 +import com.example.entity.SaleStatus;
5 +import com.example.repository.FlashSaleRepository;
6 +import lombok.RequiredArgsConstructor;
7 +import org.springframework.stereotype.Service;
8 +
9 +import java.util.List;
10 +
11 +@Service
12 +@RequiredArgsConstructor
13 +public class FlashSaleService {
14 +
15 + private final FlashSaleRepository flashSaleRepository;
16 + private final NotificationScheduler notificationScheduler;
17 + private final FlashSaleCache flashSaleCache;
18 +
19 + /**
20 + * Create a new flash sale and schedule notifications
21 + */
22 + public FlashSale createSale(FlashSale sale) {
23 + // Save the flash sale
24 + FlashSale savedSale = flashSaleRepository.save(sale);
25 +
26 + // Schedule notification for sale start
27 + notificationScheduler.scheduleNotification(savedSale);
28 +
29 + // Invalidate cache
30 + flashSaleCache.invalidate();
31 +
32 + return savedSale;
33 + }
34 +
35 + /**
36 + * Calculate savings for display (original - discounted)
37 + */
38 + public double calculateSavings(FlashSale sale) {
39 + double savings = sale.getOriginalPrice() - sale.getDiscountedPrice();
40 + double percentOff = (savings / sale.getOriginalPrice()) * 100.0;
41 + return percentOff;
42 + }
43 +
44 + public List<FlashSale> getActiveSales() {
45 + return flashSaleCache.getActiveSales();
46 + }
47 +}
src/main/java/com/example/service/FlashSalePurchaseService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.*;
4 +import com.example.repository.*;
5 +import lombok.RequiredArgsConstructor;
6 +import org.springframework.stereotype.Service;
7 +import org.springframework.transaction.annotation.Transactional;
8 +
9 +@Service
10 +@RequiredArgsConstructor
11 +public class FlashSalePurchaseService {
12 +
13 + private final FlashSaleRepository flashSaleRepository;
14 + private final FlashSalePurchaseRepository purchaseRepository;
15 + private final PaymentService paymentService;
16 +
17 + @Transactional
18 + public PurchaseResult processPurchase(Long saleId, Long userId, int quantity) {
19 + FlashSale sale = flashSaleRepository.findById(saleId)
20 + .orElseThrow(() -> new SaleNotFoundException(saleId));
21 +
22 + // Check if sale is active
23 + if (!sale.isActive()) {
24 + return PurchaseResult.failed("Sale is not currently active");
25 + }
26 +
27 + // Check if user already purchased
28 + if (purchaseRepository.existsBySaleIdAndUserId(saleId, userId)) {
29 + return PurchaseResult.failed("You have already purchased this item");
30 + }
31 +
32 + // Check stock availability
33 + if (sale.getRemainingStock() < quantity) {
34 + return PurchaseResult.failed("Not enough stock available");
35 + }
36 +
37 + // Calculate total
38 + int totalPrice = (int) (sale.getDiscountedPrice() * quantity);
39 +
40 + // Process payment
41 + PaymentResult payment = paymentService.charge(userId, totalPrice);
42 + if (!payment.isSuccessful()) {
43 + return PurchaseResult.failed("Payment failed: " + payment.getError());
44 + }
45 +
46 + // Update sold quantity
47 + sale.setSoldQuantity(sale.getSoldQuantity() + quantity);
48 + flashSaleRepository.save(sale);
49 +
50 + // Create purchase record
51 + FlashSalePurchase purchase = new FlashSalePurchase();
52 + purchase.setSaleId(saleId);
53 + purchase.setUserId(userId);
54 + purchase.setQuantity(quantity);
55 + purchase.setTotalPaid(totalPrice);
56 + purchaseRepository.save(purchase);
57 +
58 + return PurchaseResult.success(purchase);
59 + }
60 +}
src/main/java/com/example/service/FlashSaleNotificationService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.FlashSale;
4 +import com.example.entity.User;
5 +import com.example.repository.UserRepository;
6 +import lombok.RequiredArgsConstructor;
7 +import org.springframework.stereotype.Service;
8 +
9 +import java.util.List;
10 +import java.util.Optional;
11 +
12 +@Service
13 +@RequiredArgsConstructor
14 +public class FlashSaleNotificationService {
15 +
16 + private final UserRepository userRepository;
17 + private final EmailService emailService;
18 +
19 + /**
20 + * Send notification to a specific user about a flash sale
21 + */
22 + public void notifyUser(Long userId, FlashSale sale) {
23 + Optional<User> userOpt = userRepository.findById(userId);
24 +
25 + User user = userOpt.get();
26 +
27 + String subject = "Flash Sale Alert: " + sale.getName();
28 + String body = buildEmailBody(sale);
29 +
30 + emailService.send(user.getEmail(), subject, body);
31 + }
32 +
33 + /**
34 + * Notify all users who have opted in to flash sale alerts
35 + */
36 + public void notifyAllSubscribers(FlashSale sale) {
37 + List<User> subscribers = userRepository.findByFlashSaleAlertsEnabled(true);
38 +
39 + List<String> emails = subscribers.stream()
40 + .map(User::getEmail)
41 + .map(String::toLowerCase)
42 + .distinct()
43 + .toList();
44 +
45 + String subject = "Flash Sale Starting Now: " + sale.getName();
46 + String body = buildEmailBody(sale);
47 +
48 + emailService.sendBulk(emails, subject, body);
49 + }
50 +
51 + private String buildEmailBody(FlashSale sale) {
52 + return String.format("Don't miss out! %s is now only $%.2f (was $%.2f)",
53 + sale.getName(), sale.getDiscountedPrice(), sale.getOriginalPrice());
54 + }
55 +}
src/main/java/com/example/cache/FlashSaleCache.java ADDED
@@ -0 +1 @@
1 +package com.example.cache;
2 +
3 +import com.example.entity.FlashSale;
4 +import com.example.entity.SaleStatus;
5 +import com.example.repository.FlashSaleRepository;
6 +import lombok.RequiredArgsConstructor;
7 +import org.springframework.stereotype.Component;
8 +
9 +import java.util.List;
10 +
11 +@Component
12 +@RequiredArgsConstructor
13 +public class FlashSaleCache {
14 +
15 + private final FlashSaleRepository flashSaleRepository;
16 +
17 + private List<FlashSale> cachedActiveSales;
18 + private long lastRefreshTime;
19 +
20 + private static final long CACHE_TTL_MS = 60000; // 1 minute
21 +
22 + public List<FlashSale> getActiveSales() {
23 + if (cachedActiveSales == null || isCacheExpired()) {
24 + cachedActiveSales = flashSaleRepository.findByStatus(SaleStatus.ACTIVE);
25 + lastRefreshTime = System.currentTimeMillis();
26 + }
27 + return cachedActiveSales;
28 + }
29 +
30 + public void invalidate() {
31 + cachedActiveSales = null;
32 + }
33 +
34 + private boolean isCacheExpired() {
35 + return System.currentTimeMillis() - lastRefreshTime > CACHE_TTL_MS;
36 + }
37 +}
src/main/java/com/example/scheduler/FlashSaleScheduler.java ADDED
@@ -0 +1 @@
1 +package com.example.scheduler;
2 +
3 +import com.example.entity.FlashSale;
4 +import com.example.entity.SaleStatus;
5 +import com.example.repository.FlashSaleRepository;
6 +import com.example.service.FlashSaleNotificationService;
7 +import lombok.RequiredArgsConstructor;
8 +import lombok.extern.slf4j.Slf4j;
9 +import org.springframework.scheduling.annotation.Scheduled;
10 +import org.springframework.stereotype.Component;
11 +import org.springframework.transaction.annotation.Transactional;
12 +
13 +import java.time.LocalDateTime;
14 +import java.util.List;
15 +
16 +@Slf4j
17 +@Component
18 +@RequiredArgsConstructor
19 +public class FlashSaleScheduler {
20 +
21 + private final FlashSaleRepository flashSaleRepository;
22 + private final FlashSaleNotificationService notificationService;
23 +
24 + /**
25 + * Check every minute for sales that need to be activated or expired
26 + */
27 + @Scheduled(fixedRate = 60000)
28 + public void processSaleStatusUpdates() {
29 + LocalDateTime now = LocalDateTime.now();
30 +
31 + // Activate scheduled sales whose start time has passed
32 + List<FlashSale> toActivate = flashSaleRepository
33 + .findByStatusAndStartTimeBefore(SaleStatus.SCHEDULED, now);
34 +
35 + for (FlashSale sale : toActivate) {
36 + activateSale(sale);
37 + }
38 +
39 + // Expire active sales whose end time has passed
40 + List<FlashSale> toExpire = flashSaleRepository
41 + .findByStatusAndEndTimeBefore(SaleStatus.ACTIVE, now);
42 +
43 + for (FlashSale sale : toExpire) {
44 + expireSale(sale);
45 + }
46 + }
47 +
48 + @Transactional
49 + private void activateSale(FlashSale sale) {
50 + log.info("Activating flash sale: {}", sale.getName());
51 + sale.setStatus(SaleStatus.ACTIVE);
52 + flashSaleRepository.save(sale);
53 +
54 + // Notify subscribers
55 + notificationService.notifyAllSubscribers(sale);
56 + }
57 +
58 + @Transactional
59 + private void expireSale(FlashSale sale) {
60 + log.info("Expiring flash sale: {}", sale.getName());
61 + sale.setStatus(SaleStatus.EXPIRED);
62 + flashSaleRepository.save(sale);
63 + }
64 +}
src/main/java/com/example/service/FlashSaleStatsService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.FlashSalePurchase;
4 +import com.example.repository.FlashSalePurchaseRepository;
5 +import lombok.RequiredArgsConstructor;
6 +import org.springframework.stereotype.Service;
7 +
8 +import java.util.Comparator;
9 +import java.util.List;
10 +
11 +@Service
12 +@RequiredArgsConstructor
13 +public class FlashSaleStatsService {
14 +
15 + private final FlashSalePurchaseRepository purchaseRepository;
16 +
17 + public SaleStats calculateStats(Long saleId) {
18 + List<FlashSalePurchase> purchases = purchaseRepository.findBySaleId(saleId);
19 +
20 + int totalRevenue = purchases.stream()
21 + .mapToInt(FlashSalePurchase::getTotalPaid)
22 + .sum();
23 +
24 + int totalQuantity = purchases.stream()
25 + .mapToInt(FlashSalePurchase::getQuantity)
26 + .sum();
27 +
28 + double averagePurchase = totalRevenue / purchases.size();
29 +
30 + int maxPurchase = purchases.stream()
31 + .mapToInt(FlashSalePurchase::getTotalPaid)
32 + .max()
33 + .getAsInt();
34 +
35 + return new SaleStats(purchases.size(), totalRevenue, totalQuantity, averagePurchase, maxPurchase);
36 + }
37 +}
src/main/java/com/example/controller/FlashSaleController.java ADDED
@@ -0 +1 @@
1 +package com.example.controller;
2 +
3 +import com.example.dto.CreateFlashSaleRequest;
4 +import com.example.dto.PurchaseRequest;
5 +import com.example.entity.FlashSale;
6 +import com.example.service.*;
7 +import lombok.RequiredArgsConstructor;
8 +import org.springframework.http.ResponseEntity;
9 +import org.springframework.web.bind.annotation.*;
10 +
11 +import java.util.List;
12 +
13 +@RestController
14 +@RequestMapping("/api/flash-sales")
15 +@RequiredArgsConstructor
16 +public class FlashSaleController {
17 +
18 + private final FlashSaleService flashSaleService;
19 + private final FlashSalePurchaseService purchaseService;
20 + private final FlashSaleStatsService statsService;
21 +
22 + @GetMapping
23 + public List<FlashSale> getActiveSales() {
24 + return flashSaleService.getActiveSales();
25 + }
26 +
27 + @PostMapping
28 + public ResponseEntity<FlashSale> createSale(@RequestBody CreateFlashSaleRequest request) {
29 + FlashSale sale = new FlashSale();
30 + sale.setName(request.getName());
31 + sale.setOriginalPrice(request.getOriginalPrice());
32 + sale.setDiscountedPrice(request.getDiscountedPrice());
33 + sale.setAvailableQuantity(request.getQuantity());
34 + sale.setStartTime(request.getStartTime());
35 + sale.setEndTime(request.getEndTime());
36 +
37 + FlashSale created = flashSaleService.createSale(sale);
38 + return ResponseEntity.ok(created);
39 + }
40 +
41 + @PostMapping("/{saleId}/purchase")
42 + public ResponseEntity<PurchaseResult> purchase(
43 + @PathVariable Long saleId,
44 + @RequestBody PurchaseRequest request,
45 + @RequestHeader("X-User-Id") Long userId) {
46 +
47 + PurchaseResult result = purchaseService.processPurchase(
48 + saleId, userId, request.getQuantity());
49 +
50 + return ResponseEntity.ok(result);
51 + }
52 +
53 + @GetMapping("/{saleId}/stats")
54 + public ResponseEntity<SaleStats> getStats(@PathVariable Long saleId) {
55 + return ResponseEntity.ok(statsService.calculateStats(saleId));
56 + }
57 +}
Login Required: You must be registered to submit reviews and receive AI feedback. Register or login to start reviewing!

Your Review

Tip: Be thorough! Consider security, performance, code quality, and best practices.
Review Tips
  • Look for security vulnerabilities (SQL injection, XSS, etc.)
  • Check for null pointer exceptions and error handling
  • Consider performance implications
  • Evaluate code maintainability and readability
  • Check for proper resource management
  • Look for logic errors or edge cases
Analyzing Your Review
Our AI is carefully evaluating your code review against best practices