DiscountApprovalService.java
package com.university.bookstore.chain;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.university.bookstore.model.Material;
/**
* Service for managing discount approval using the Chain of Responsibility pattern.
* Builds and manages the approval chain and processes discount requests.
*
* <p>This service demonstrates the Chain of Responsibility pattern by creating
* a hierarchy of approval handlers and processing requests through the chain.</p>
*
* @author Navid Mohaghegh
* @version 3.0
* @since 2024-09-15
*/
public class DiscountApprovalService {
private static final Logger LOGGER = LoggerFactory.getLogger(DiscountApprovalService.class);
private final DiscountHandler chain;
private final List<DiscountRequest> processedRequests;
/**
* Creates a new discount approval service with the default chain.
*/
public DiscountApprovalService() {
this.chain = buildDefaultChain();
this.processedRequests = new ArrayList<>();
}
/**
* Creates a new discount approval service with a custom chain.
*
* @param chain the approval chain to use
*/
public DiscountApprovalService(DiscountHandler chain) {
this.chain = Objects.requireNonNull(chain, "Chain cannot be null");
this.processedRequests = new ArrayList<>();
}
/**
* Processes a discount request through the approval chain.
*
* @param material the material for which discount is requested
* @param discountPercent the discount percentage (0.0 to 100.0)
* @param customerId the customer requesting the discount
* @param reason the reason for the discount request
* @return the processed discount request
*/
public DiscountRequest requestDiscount(Material material, double discountPercent,
String customerId, String reason) {
if (material == null) {
throw new IllegalArgumentException("Material cannot be null");
}
if (customerId == null || customerId.trim().isEmpty()) {
throw new IllegalArgumentException("Customer ID cannot be null or empty");
}
if (reason == null || reason.trim().isEmpty()) {
throw new IllegalArgumentException("Reason cannot be null or empty");
}
// Convert percentage to decimal
double discountRate = discountPercent / 100.0;
DiscountRequest request = new DiscountRequest(material, discountRate, customerId, reason);
LOGGER.info("Processing discount request:");
LOGGER.info("Material: {}", material.getTitle());
LOGGER.info("Requested discount: {}%", discountPercent);
LOGGER.info("Customer: {}", customerId);
LOGGER.info("Reason: {}", reason);
LOGGER.info("---");
// Process through the chain
chain.handleRequest(request);
// Store the processed request
processedRequests.add(request);
// Print result
if (request.isApproved()) {
LOGGER.info("// [OK] APPROVED by {}", request.getApprovedBy());
LOGGER.info("Final price: ${}", String.format("%.2f", request.getDiscountedPrice()));
LOGGER.info("Savings: ${}", String.format("%.2f", request.getSavingsAmount()));
} else {
LOGGER.info("[X] REJECTED: {}", request.getRejectionReason());
}
return request;
}
/**
* Calculates the final price for an approved discount request.
*
* @param request the discount request
* @return the final price
*/
public double calculateFinalPrice(DiscountRequest request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
return request.getDiscountedPrice();
}
/**
* Gets all processed discount requests.
*
* @return list of processed requests
*/
public List<DiscountRequest> getProcessedRequests() {
return new ArrayList<>(processedRequests);
}
/**
* Gets approved discount requests.
*
* @return list of approved requests
*/
public List<DiscountRequest> getApprovedRequests() {
return processedRequests.stream()
.filter(DiscountRequest::isApproved)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Gets rejected discount requests.
*
* @return list of rejected requests
*/
public List<DiscountRequest> getRejectedRequests() {
return processedRequests.stream()
.filter(request -> !request.isApproved())
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Gets discount requests for a specific customer.
*
* @param customerId the customer ID
* @return list of requests for the customer
*/
public List<DiscountRequest> getRequestsForCustomer(String customerId) {
return processedRequests.stream()
.filter(request -> Objects.equals(request.getCustomerId(), customerId))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Gets discount requests for a specific material.
*
* @param material the material
* @return list of requests for the material
*/
public List<DiscountRequest> getRequestsForMaterial(Material material) {
return processedRequests.stream()
.filter(request -> Objects.equals(request.getMaterial(), material))
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
/**
* Gets approval statistics.
*
* @return approval statistics
*/
public ApprovalStats getApprovalStats() {
int totalRequests = processedRequests.size();
int approvedRequests = (int) processedRequests.stream()
.filter(DiscountRequest::isApproved)
.count();
int rejectedRequests = totalRequests - approvedRequests;
double approvalRate = totalRequests > 0 ? (double) approvedRequests / totalRequests : 0.0;
double totalSavings = processedRequests.stream()
.filter(DiscountRequest::isApproved)
.mapToDouble(DiscountRequest::getSavingsAmount)
.sum();
return new ApprovalStats(
totalRequests,
approvedRequests,
rejectedRequests,
approvalRate,
totalSavings
);
}
/**
* Clears all processed requests.
*/
public void clearRequests() {
processedRequests.clear();
}
/**
* Gets the approval chain information.
*
* @return chain information
*/
public String getChainInfo() {
return chain.getHandlerInfo();
}
/**
* Builds the default approval chain: Manager -> Director -> VP.
*
* @return the default approval chain
*/
private DiscountHandler buildDefaultChain() {
DiscountHandler manager = new ManagerHandler();
DiscountHandler director = new DirectorHandler();
DiscountHandler vp = new VPHandler();
// Set up the chain
manager.setNext(director);
director.setNext(vp);
return manager;
}
/**
* Statistics class for approval analysis.
*/
public static class ApprovalStats {
private final int totalRequests;
private final int approvedRequests;
private final int rejectedRequests;
private final double approvalRate;
private final double totalSavings;
public ApprovalStats(int totalRequests, int approvedRequests, int rejectedRequests,
double approvalRate, double totalSavings) {
this.totalRequests = totalRequests;
this.approvedRequests = approvedRequests;
this.rejectedRequests = rejectedRequests;
this.approvalRate = approvalRate;
this.totalSavings = totalSavings;
}
public int getTotalRequests() { return totalRequests; }
public int getApprovedRequests() { return approvedRequests; }
public int getRejectedRequests() { return rejectedRequests; }
public double getApprovalRate() { return approvalRate; }
public double getTotalSavings() { return totalSavings; }
@Override
public String toString() {
return String.format("ApprovalStats[Total=%d, Approved=%d, Rejected=%d, Rate=%.1f%%, Savings=$%.2f]",
totalRequests, approvedRequests, rejectedRequests, approvalRate * 100, totalSavings);
}
}
@Override
public String toString() {
return String.format("DiscountApprovalService[Chain=%s, Requests=%d]",
chain.getHandlerName(), processedRequests.size());
}
}