StatisticsController for SalesVolume and SalesRevenue, fix OrderController

This commit is contained in:
Tim
2025-06-12 22:38:24 +02:00
parent 1e94fd2e86
commit cb99839e39
18 changed files with 195 additions and 21 deletions

View File

@@ -41,4 +41,10 @@ public class ControllerPathConfig {
//ReviewController
public static final String REVIEW_BASE = "/review";
public static final String REVIEW_GET_ALL = REVIEW_BASE + "/all";
//StatisticsController
private static final String STATISTICS_BASE = "/statistics";
public static final String STATISTICS_VOLUME = STATISTICS_BASE + "/volume";
public static final String STATISTICS_REVENUE = STATISTICS_BASE + "/revenue";
}

View File

@@ -13,4 +13,5 @@ public class ParameterConfig {
public static final String PARAM_IMAGE = "image";
public static final String PARAM_STATUS = "status";
public static final String PARAM_STANDARD = "standard";
public static final String PARAM_SESSION = "session";
}

View File

@@ -63,7 +63,8 @@ public class OrderController {
log.warn("[{}] failed Validation, sending bad request", request.getRequestURI());
return ResponseEntity.badRequest().body(false);
}
order.setId(null);
order.getOrderItems().forEach(orderItem -> orderItem.setId(null));
Order saved = orderService.save(order);
return ResponseEntity.ok(saved != null);
}

View File

@@ -0,0 +1,58 @@
package de.htwsaar.webshop.controller;
import de.htwsaar.webshop.model.MonthlyCatModel;
import de.htwsaar.webshop.service.SessionService;
import de.htwsaar.webshop.service.StatisticsService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import static de.htwsaar.webshop.config.ControllerPathConfig.STATISTICS_REVENUE;
import static de.htwsaar.webshop.config.ControllerPathConfig.STATISTICS_VOLUME;
import static de.htwsaar.webshop.config.ParameterConfig.PARAM_EMAIL;
import static de.htwsaar.webshop.config.ParameterConfig.PARAM_SESSION;
import static de.htwsaar.webshop.util.LoggerUtil.logRequest;
@RestController
@Slf4j
public class StatisticsController {
private final StatisticsService statisticsService;
private final SessionService sessionService;
public StatisticsController(StatisticsService statisticsService, SessionService sessionService) {
this.statisticsService = statisticsService;
this.sessionService = sessionService;
}
@RequestMapping(value = STATISTICS_VOLUME, method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<MonthlyCatModel<Integer>> getMonthlySalesVolume(HttpServletRequest request,
@RequestParam(value = PARAM_SESSION) UUID session,
@RequestParam(value = PARAM_EMAIL) String email) {
logRequest(request);
if (!sessionService.isAdmin(session, email)) {
log.warn("Invalid session requesting Admin {}", session);
return ResponseEntity.status(403).build();
}
return ResponseEntity.ok(statisticsService.getSalesVolume());
}
@RequestMapping(value = STATISTICS_REVENUE, method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<MonthlyCatModel<Integer>> getMonthlyRevenue(HttpServletRequest request,
@RequestParam(value = PARAM_SESSION) UUID token,
@RequestParam(value = PARAM_EMAIL) String email) {
logRequest(request);
if (!sessionService.isAdmin(token, email)) {
log.warn("Invalid session requesting Admin {}", token);
return ResponseEntity.status(403).build();
}
return ResponseEntity.ok(statisticsService.getSalesRevenue());
}
}

View File

@@ -0,0 +1,15 @@
package de.htwsaar.webshop.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Map;
@Getter
@Setter
@AllArgsConstructor
public class MonthlyCatModel<T extends Number> {
Map<Long, Map<String, T>> monthCategoryMap;
}

View File

@@ -9,4 +9,6 @@ import java.util.List;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> getOrdersByCustomer_Id(Long customerId);
List<Order> getOrdersByTimeBetween(Long timeAfter, Long timeBefore);
}

View File

@@ -11,7 +11,7 @@ import org.hibernate.type.NumericBooleanConverter;
@AllArgsConstructor
@ToString
@Entity
@Table(name = "FarmImages")
@Table(name = "Farm_Images")
public class FarmImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@@ -31,10 +31,9 @@ public class Order {
@Column(name = "status", nullable = false)
private OrderStatus status;
@OneToMany(mappedBy = "order", orphanRemoval = true, fetch = FetchType.LAZY, cascade = CascadeType.ALL, targetEntity = OrderItem.class)
@OneToMany(mappedBy = "order", orphanRemoval = true, fetch = FetchType.EAGER, cascade = CascadeType.ALL, targetEntity = OrderItem.class)
private List<OrderItem> orderItems;
public OrderModel toModel() {
return new OrderModel(id, customer.getId(), time, status, orderItems.stream().map(OrderItem::toModel).toList());
}

View File

@@ -14,7 +14,7 @@ import org.hibernate.annotations.OnDelete;
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "OrderItems")
@Table(name = "Order_Items")
public class OrderItem {
@Id
@@ -25,8 +25,8 @@ public class OrderItem {
@Min(value = 1, message = "Amount must be at least 1")
private Integer amount;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("orderId")
@ManyToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL, targetEntity = Order.class)
@MapsId("order_id")
@JoinColumn(name = "order_id", referencedColumnName = "id", nullable = false)
@OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
private Order order;

View File

@@ -4,6 +4,7 @@ import de.htwsaar.webshop.model.OrderModel;
import de.htwsaar.webshop.repository.entities.Order;
import java.util.List;
import java.util.Map;
public interface OrderService {
Order save(Order order);
@@ -15,4 +16,6 @@ public interface OrderService {
Order getOrderById(Long orderId);
List<Order> getAllOrders(Long customerId);
Map<Long, List<Order>> getTimeSortedOrders(long fromMilli, long months);
}

View File

@@ -18,5 +18,5 @@ public interface SessionService {
boolean isValid(UUID token, String email);
boolean isAdmin(Session session, String email);
boolean isAdmin(UUID token, String email);
}

View File

@@ -0,0 +1,10 @@
package de.htwsaar.webshop.service;
import de.htwsaar.webshop.model.MonthlyCatModel;
public interface StatisticsService {
MonthlyCatModel<Integer> getSalesVolume();
MonthlyCatModel<Integer> getSalesRevenue();
}

View File

@@ -8,11 +8,14 @@ import de.htwsaar.webshop.repository.entities.OrderItem;
import de.htwsaar.webshop.service.ArticleService;
import de.htwsaar.webshop.service.CustomerService;
import de.htwsaar.webshop.service.OrderService;
import de.htwsaar.webshop.util.TimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@@ -69,4 +72,19 @@ public class OrderServiceImpl implements OrderService {
public List<Order> getAllOrders(Long customerId) {
return orderRepository.getOrdersByCustomer_Id(customerId);
}
@Override
public Map<Long, List<Order>> getTimeSortedOrders(long fromMilli, long months) {
Map<Long, List<Order>> orders = new HashMap<>();
long lower = fromMilli;
long upper;
while (months > 0) {
upper = fromMilli + TimeUtil.monthLength(fromMilli);
log.info("Getting Orders from {} to {}", lower, upper);
orders.put(fromMilli, orderRepository.getOrdersByTimeBetween(lower, upper));
lower = upper;
months--;
}
return orders;
}
}

View File

@@ -72,11 +72,11 @@ public class SessionServiceImpl implements SessionService {
}
@Override
public boolean isAdmin(Session session, String email) {
if(session == null || email == null) {
public boolean isAdmin(UUID token, String email) {
if(token == null || email == null) {
return false;
}
if(!isValid(session, email)) {
if(!isValid(token, email)) {
log.warn("Invalid Session with email {} trying to access Admin Services", email);
return false;
}

View File

@@ -0,0 +1,59 @@
package de.htwsaar.webshop.service.impl;
import de.htwsaar.webshop.model.MonthlyCatModel;
import de.htwsaar.webshop.repository.entities.Order;
import de.htwsaar.webshop.repository.entities.OrderItem;
import de.htwsaar.webshop.service.OrderService;
import de.htwsaar.webshop.service.StatisticsService;
import de.htwsaar.webshop.util.TimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@Slf4j
public class StatisticsServiceImpl implements StatisticsService {
private final OrderService orderService;
@Autowired
public StatisticsServiceImpl(OrderService orderService) {
this.orderService = orderService;
}
//don't ask pls
//returns Map<unix milli timestamp, Map<Category, T>>
private <T extends Number> MonthlyCatModel<T> getMonthCategoryMap(Function<OrderItem, T> mappingFunction,
BinaryOperator<T> reduceFunction) {
return new MonthlyCatModel<>(
orderService.getTimeSortedOrders(TimeUtil.nowMonthsAgo(12), 12).entrySet().stream()
.map(entry -> Map.entry(entry.getKey(),
entry.getValue().stream()
.map(Order::getOrderItems)
.reduce(new ArrayList<>(), (l, r) -> {
l.addAll(r);
return l;
})
.stream()
.map(orderItem -> Map.entry(orderItem.getArticle().getCategory(), mappingFunction.apply(orderItem)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, reduceFunction))
))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
);
}
@Override
public MonthlyCatModel<Integer> getSalesVolume() {
return getMonthCategoryMap(OrderItem::getAmount, Integer::sum);
}
@Override
public MonthlyCatModel<Integer> getSalesRevenue() {
return getMonthCategoryMap(item -> item.getArticle().getPrice100(), Integer::sum);
}
}

View File

@@ -1,5 +1,9 @@
package de.htwsaar.webshop.util;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
/**
* Helper Class for Unix-Millis related Things
*/
@@ -41,13 +45,11 @@ public class TimeUtil {
return System.currentTimeMillis() + (days * MILLIS_TO_DAY);
}
/**
* Check whether the Unix-Millis are valid
*
* @param time the Millis to Check
* @return whether the Time <b>can</b> be valid
*/
public static boolean isValidTime(long time) {
return time >= VALID_MIN_MILLIS_TIMESTAMP;
public static long nowMonthsAgo(long months) {
return LocalDateTime.now().minusMonths(months).atZone(ZoneId.systemDefault()).toEpochSecond();
}
public static long monthLength(long epoch) {
return LocalDateTime.ofEpochSecond(epoch, 0, ZoneOffset.UTC).getMonth().length(false) * MILLIS_TO_DAY;
}
}

View File

@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS Images
FOREIGN KEY (article_id) REFERENCES Articles (id)
);
CREATE TABLE IF NOT EXISTS FarmImages
CREATE TABLE IF NOT EXISTS Farm_Images
(
id INTEGER PRIMARY KEY NOT NULL,
article_id INTEGER NOT NULL,
@@ -82,7 +82,7 @@ CREATE TABLE IF NOT EXISTS Orders
ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS OrderItems
CREATE TABLE IF NOT EXISTS Order_Items
(
id INTEGER NOT NULL PRIMARY KEY,
order_id INTEGER NOT NULL,