Skip to content

Commit

Permalink
Merge pull request #505 from WISVCH/feature/parent-products
Browse files Browse the repository at this point in the history
Implement parent products
  • Loading branch information
robertdijk authored Sep 25, 2024
2 parents 0bb248b + 0275ba9 commit 31ebee5
Show file tree
Hide file tree
Showing 21 changed files with 345 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ abstract class DashboardController {
/** Object key product. */
static final String OBJ_PRODUCT = "product";

/** Object key parentProducts. */
static final String OBJ_PARENT_PRODUCTS = "parentProducts";

/** Object key tasks. */
static final String OBJ_TASKS = "tasks";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,19 @@ public String create(RedirectAttributes redirect, @ModelAttribute Product produc
*
* @return thymeleaf template path
*/
@GetMapping("/edit/{key}")
@GetMapping({"/edit/{key}","/edit/{key}/"})
public String edit(Model model, RedirectAttributes redirect, @PathVariable String key) {
try {
if (!model.containsAttribute(OBJ_PRODUCT)) {
model.addAttribute(OBJ_PRODUCT, productService.getByKey(key));
}
if (!model.containsAttribute(OBJ_PARENT_PRODUCTS)) {
model.addAttribute(OBJ_PARENT_PRODUCTS,
productService.getPossibleParentProductsByProduct(
(Product) model.getAttribute(OBJ_PRODUCT)
)
);
}

return "admin/products/product";
} catch (ProductNotFoundException e) {
Expand All @@ -164,7 +171,7 @@ public String edit(Model model, RedirectAttributes redirect, @PathVariable Strin
*
* @return String
*/
@PostMapping("/edit/{key}")
@PostMapping({"/edit/{key}","/edit/{key}/"})
public String update(RedirectAttributes redirect, @ModelAttribute Product product, @PathVariable String key) {
try {
product.setKey(key);
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/ch/wisv/events/admin/utils/ProductTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public enum ProductTemplate {

/** Product maxSoldperCustomer. */
@Getter
private final int maxSolPerCustomer;
private final int maxSoldPerCustomer;

/** Product chOnly. */
@Getter
Expand All @@ -53,23 +53,23 @@ public enum ProductTemplate {
* @param title of type String
* @param cost of type double
* @param vatRate of type VatRate
* @param maxSolPerCustomer of type int
* @param maxSoldPerCustomer of type int
* @param chOnly of type boolean
* @param reservable of type boolean
*/
ProductTemplate(
String templateName, String title,
double cost,
VatRate vatRate,
int maxSolPerCustomer,
int maxSoldPerCustomer,
boolean chOnly,
boolean reservable
) {
this.templateName = templateName;
this.title = title;
this.cost = cost;
this.vatRate = vatRate;
this.maxSolPerCustomer = maxSolPerCustomer;
this.maxSoldPerCustomer = maxSoldPerCustomer;
this.chOnly = chOnly;
this.reservable = reservable;
}
Expand All @@ -85,7 +85,7 @@ public String toJson() {
object.put("title", this.getTitle());
object.put("cost", this.getCost());
object.put("vatRate", this.getVatRate());
object.put("maxSoldPerCustomer", this.getMaxSolPerCustomer());
object.put("maxSoldPerCustomer", this.getMaxSoldPerCustomer());
object.put("chOnly", this.isChOnly());
object.put("reservable", this.isReservable());

Expand Down
71 changes: 67 additions & 4 deletions src/main/java/ch/wisv/events/core/model/product/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;

import ch.wisv.events.core.model.event.Event;
Expand Down Expand Up @@ -102,12 +106,28 @@ public class Product {

@ManyToOne
@JoinTable(name = "event_products", joinColumns = @JoinColumn(name = "products_id"))
@JsonIgnore
public Event event;

/**
* Parent Product. Shares the product availability.
*/
@ManyToOne
@JoinColumn(name = "parent_product_id")
public Product parentProduct;

/**
* Field childProducts.
*/
@OneToMany(mappedBy = "parentProduct", cascade = CascadeType.MERGE, targetEntity = Product.class, fetch = FetchType.LAZY)
@JsonIgnore
public List<Product> childProducts;

/**
* Field productList.
*/
@OneToMany(cascade = CascadeType.MERGE, targetEntity = Product.class, fetch = FetchType.EAGER)
@JsonIgnore
public List<Product> products;

/**
Expand Down Expand Up @@ -196,13 +216,13 @@ public Product(
* @return progress of event
*/
public double calcProgress() {
if (this.maxSold == null) {
if (this.getMaxSold() == null) {
return 100.d;
} else if (this.sold == 0) {
} else if (this.getTotalSold() == 0) {
return 0.d;
}

return Math.round((((double) this.sold / (double) this.maxSold) * 100.d) * 100.d) / 100.d;
return Math.round((((double) this.getTotalSold() / (double) this.getMaxSold()) * 100.d) * 100.d) / 100.d;
}

/**
Expand All @@ -229,7 +249,50 @@ public void increaseReserved(int amount) {
* @return boolean
*/
public boolean isSoldOut() {
return this.maxSold != null && this.sold >= this.maxSold;
return this.getMaxSold() != null && this.getTotalSold() >= this.getMaxSold();
}

public Integer getMaxSold() {
// If the product has a parent product, return the maxSold of the parent product.
if (this.parentProduct != null) {
return this.parentProduct.getMaxSold();
}

return this.maxSold;
}

public Integer getMaxSoldPerCustomer() {
// If the product has a parent product, return the maxSoldPerCustomer of the parent product.
if (this.parentProduct != null) {
return this.parentProduct.getMaxSoldPerCustomer();
}

return this.maxSoldPerCustomer;
}

public Integer getTotalSold() {
// If the product has a parent product, return the total sold of the parent product.
if (this.parentProduct != null) {
return this.parentProduct.getTotalSold();
}

int totalSold = this.sold;
if (this.childProducts != null) {
for (Product childProduct : this.childProducts) {
totalSold += childProduct.getSold();
}
}

return totalSold;
}

// Custom toString
@Override
public String toString() {
return "Product{" +
"id=" + id +
", title='" + title + '\'' +
", description='" + description + '\'' +
'}';
}
}
10 changes: 10 additions & 0 deletions src/main/java/ch/wisv/events/core/repository/TicketRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ public interface TicketRepository extends JpaRepository<Ticket, Integer> {
*/
List<Ticket> findAllByProductAndOwner(Product product, Customer owner);

/**
* Find all Ticket by Products and Customer.
*
* @param products of type List<Product></Product>
* @param owner of type Customer
*
* @return List
*/
List<Ticket> findAllByProductInAndOwner(List<Product> products, Customer owner);

/**
* Find all Ticket by Product.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ch.wisv.events.core.model.product.Product;
import ch.wisv.events.core.repository.OrderRepository;
import ch.wisv.events.core.service.event.EventService;
import ch.wisv.events.core.service.product.ProductService;
import ch.wisv.events.core.service.ticket.TicketService;
import java.time.LocalDateTime;
import java.util.List;
Expand All @@ -31,6 +32,9 @@ public class OrderValidationServiceImpl implements OrderValidationService {
/** TicketService. */
private final TicketService ticketService;

/** ProductService. */
private final ProductService productService;

/** EventService. */
private final EventService eventService;

Expand All @@ -42,9 +46,10 @@ public class OrderValidationServiceImpl implements OrderValidationService {
* @param eventService of type EventService
*/
@Autowired
public OrderValidationServiceImpl(OrderRepository orderRepository, TicketService ticketService, EventService eventService) {
public OrderValidationServiceImpl(OrderRepository orderRepository, TicketService ticketService, ProductService productService, EventService eventService) {
this.orderRepository = orderRepository;
this.ticketService = ticketService;
this.productService = productService;
this.eventService = eventService;
}

Expand Down Expand Up @@ -80,16 +85,23 @@ public void assertOrderIsValidForCustomer(Order order, Customer customer) throws
continue;
}

int ticketSold = ticketService.getAllByProductAndCustomer(orderProduct.getProduct(), customer).size();
List<Product> relatedProducts = productService.getRelatedProducts(orderProduct.getProduct());

int ticketSold = ticketService.getAllByProductsAndCustomer(relatedProducts, customer).size();

ticketSold += reservationOrders.stream()
.mapToInt(reservationOrder -> reservationOrder.getOrderProducts().stream()
.filter(reservationOrderProduct -> orderProduct.getProduct().equals(reservationOrderProduct.getProduct()))
.filter(reservationOrderProduct -> relatedProducts.contains(reservationOrderProduct.getProduct()))
.mapToInt(reservationOrderProduct -> reservationOrderProduct.getAmount().intValue())
.sum()
).sum();

if (ticketSold + orderProduct.getAmount() > maxSoldPerCustomer) {
int tryingToOrder = order.getOrderProducts().stream()
.filter(relatedOrderProduct -> relatedProducts.contains(relatedOrderProduct.getProduct()))
.mapToInt(relatedOrderProduct -> relatedOrderProduct.getAmount().intValue())
.sum();

if (ticketSold + tryingToOrder > maxSoldPerCustomer) {
throw new OrderExceedCustomerLimitException(maxSoldPerCustomer - ticketSold);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ public interface ProductService {
*/
List<Product> getAvailableProducts();

/**
* Get possible parent products.
*
* @return Collection of Products
*/
List<Product> getPossibleParentProductsByProduct(Product product);

/**
* Get the parent, children, and sibling products, including the original product
*
* @return Collection of Products
*/
List<Product> getRelatedProducts(Product product);

/**
* Get Product by Key.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
import ch.wisv.events.core.model.product.Product;
import ch.wisv.events.core.repository.ProductRepository;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -53,10 +51,47 @@ public List<Product> getAllProducts() {
public List<Product> getAvailableProducts() {
return productRepository.findAllBySellStartBefore(LocalDateTime.now()).stream()
.filter(product -> (product.getSellEnd() == null || !product.getSellEnd()
.isBefore(LocalDateTime.now())) && (product.getMaxSold() == null || product.getSold() < product.getMaxSold()))
.isBefore(LocalDateTime.now())) && (product.getMaxSold() == null || product.getTotalSold() < product.getMaxSold()))
.collect(Collectors.toCollection(ArrayList::new));
}

/**
* Get possible parent products.
*
* @return Collection of Products
*/
@Override
public List<Product> getPossibleParentProductsByProduct(Product product) {
if (product.getEvent() == null) {
return new ArrayList<>();
}
return product.getEvent().getProducts().stream()
.filter(p -> p.getParentProduct() == null && !p.getKey().equals(product.getKey()))
.collect(Collectors.toList());
}

/**
* Get the parent, children, and sibling products, including the original product
*
* @return Collection of Products
*/
@Override
public List<Product> getRelatedProducts(Product product) {
List<Product> result = new ArrayList<>();
Product parent = product;
while (parent.getParentProduct() != null) {
parent = parent.getParentProduct();
}
Queue<Product> q = new LinkedList<>();
q.add(parent);
while (!q.isEmpty()) {
Product p = q.remove();
result.add(p);
q.addAll(p.getChildProducts());
}
return result;
}

/**
* Get Product by Key.
*
Expand Down Expand Up @@ -125,6 +160,7 @@ public void update(Product product) throws ProductNotFoundException, ProductInva
model.setMaxSoldPerCustomer(product.getMaxSoldPerCustomer());
model.setChOnly(product.isChOnly());
model.setReservable(product.isReservable());
model.setParentProduct(product.getParentProduct());

if (product.getSold() != 0) {
model.setSold(product.getSold());
Expand Down Expand Up @@ -197,9 +233,13 @@ private void assertIsValidProduct(Product product) throws ProductInvalidExceptio
throw new ProductInvalidException("It is not possible to add the same product twice or more!");
}

if (product.getMaxSoldPerCustomer() == null || product.getMaxSoldPerCustomer() < 1 || product.getMaxSoldPerCustomer() > 25) {
if (product.getParentProduct() == null && (product.getMaxSoldPerCustomer() == null || product.getMaxSoldPerCustomer() < 1 || product.getMaxSoldPerCustomer() > 25)) {
throw new ProductInvalidException("Max sold per customer should be between 1 and 25!");
}

if (product.getParentProduct() != null && product.getChildProducts() != null && product.getChildProducts().size() > 0) {
throw new ProductInvalidException("A product can not have a parent product and be a parent product at the same time!");
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ public interface TicketService {
*/
List<Ticket> getAllByProductAndCustomer(Product product, Customer customer);

/**
* Get all Ticket for one of multiple products and Customer.
*
* @param products of type List<Product></Product>
* @param customer of type Customer
*
* @return List of Tickets
*/
List<Ticket> getAllByProductsAndCustomer(List<Product> products, Customer customer);

/**
* Get all Ticket by a Product.
*
Expand Down
Loading

0 comments on commit 31ebee5

Please sign in to comment.