Initial commit

This commit is contained in:
Alexander Molochko
2019-03-17 08:46:15 +02:00
commit c2df4f148f
54 changed files with 1860 additions and 0 deletions

46
store-core/pom.xml Normal file
View File

@@ -0,0 +1,46 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>store-core</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<parent>
<artifactId>hexagonal-architecture</artifactId>
<groupId>com.baeldung</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../hexagonal-architecture</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.context.version}</version>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>${javax.persistence.version}</version>
</dependency>
</dependencies>
<properties>
<lombok.version>1.16.8</lombok.version>
<spring.context.version>5.1.5.RELEASE</spring.context.version>
<javax.persistence.version>2.2</javax.persistence.version>
</properties>
</project>

View File

@@ -0,0 +1,91 @@
package com.baeldung.hexagonal.store.core.context.customer.entity;
import com.baeldung.hexagonal.store.core.context.order.entity.Order;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Customer implements StoreCustomer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
String firstname, lastname;
String email;
Double balance;
@OneToMany(
mappedBy = "customer",
cascade = CascadeType.ALL,
orphanRemoval = true
)
List<Order> orders = new ArrayList<>();
@Override
public boolean isNegativeBalanceAllowed() {
return true;
}
@Override
public Double getBalance() {
return balance;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public Long getId() {
return id;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public void withdrawFunds(Double sum) {
this.balance -= sum;
}
public boolean hasEnoughFunds(Double amount) {
return this.balance >= amount;
}
public void topUpFunds(Double sum) {
this.balance += sum;
}
public List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
public void addOrder(Order order) {
this.orders.add(order);
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.hexagonal.store.core.context.customer.entity;
public interface StoreCustomer {
boolean isNegativeBalanceAllowed();
Double getBalance();
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.hexagonal.store.core.context.customer.exception;
public class CustomerNotFoundException extends RuntimeException {
public CustomerNotFoundException() {
super("Customer not found!");
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.hexagonal.store.core.context.customer.exception;
public class NotEnoughFundsException extends RuntimeException {
public NotEnoughFundsException() {
super("Not enough funds!");
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.hexagonal.store.core.context.customer.exception;
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException() {
super("Order not found!");
}
}

View File

@@ -0,0 +1,13 @@
package com.baeldung.hexagonal.store.core.context.customer.infrastructure;
import com.baeldung.hexagonal.store.core.context.customer.entity.Customer;
import java.util.Optional;
public interface CustomerDataStore {
Customer save(Customer customer);
Optional<Customer> findById(Long customerId);
Iterable<Customer> findAll();
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.hexagonal.store.core.context.customer.service;
import com.baeldung.hexagonal.store.core.context.customer.entity.Customer;
public interface CustomerService {
Iterable<Customer> getAllCustomers();
}

View File

@@ -0,0 +1,21 @@
package com.baeldung.hexagonal.store.core.context.customer.service;
import com.baeldung.hexagonal.store.core.context.customer.entity.Customer;
import com.baeldung.hexagonal.store.core.context.customer.infrastructure.CustomerDataStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CustomerServiceImpl implements CustomerService {
private CustomerDataStore customerDataStore;
@Autowired
public CustomerServiceImpl(CustomerDataStore customerDataStore) {
this.customerDataStore = customerDataStore;
}
@Override
public Iterable<Customer> getAllCustomers() {
return this.customerDataStore.findAll();
}
}

View File

@@ -0,0 +1,76 @@
package com.baeldung.hexagonal.store.core.context.order.entity;
import com.baeldung.hexagonal.store.core.context.customer.entity.Customer;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
private String status;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@OneToMany(mappedBy = "id.order", cascade = CascadeType.ALL)
private List<OrderProduct> orderProducts = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@Transient
public Double getTotalOrderPrice() {
return getOrderProducts()
.stream()
.reduce(
0D,
(aDouble, orderProduct) -> orderProduct.getTotalPrice(),
(aDouble, aDouble2) -> aDouble + aDouble2);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public List<OrderProduct> getOrderProducts() {
return orderProducts;
}
public void setOrderProducts(List<OrderProduct> orderProducts) {
this.orderProducts = orderProducts;
}
public void addOrderProduct(OrderProduct product) {
this.orderProducts.add(product);
}
@Transient
public int getNumberOfProducts() {
return this.orderProducts.size();
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}

View File

@@ -0,0 +1,85 @@
package com.baeldung.hexagonal.store.core.context.order.entity;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Transient;
@Entity
public class OrderProduct {
@EmbeddedId
private OrderProductId id;
@Column(nullable = false)
private Integer quantity;
public OrderProduct() {
super();
}
public OrderProduct(Order order, Product product, Integer quantity) {
id = new OrderProductId();
id.setOrder(order);
id.setProduct(product);
this.quantity = quantity;
}
@Transient
public Product getProduct() {
return this.id.getProduct();
}
@Transient
public Double getTotalPrice() {
return getProduct().getPrice() * getQuantity();
}
public OrderProductId getId() {
return id;
}
public void setId(OrderProductId id) {
this.id = id;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
OrderProduct other = (OrderProduct) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,97 @@
package com.baeldung.hexagonal.store.core.context.order.entity;
import javax.persistence.Embeddable;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.io.Serializable;
@Embeddable
public class OrderProductId implements Serializable {
private static final long serialVersionUID = 476151177562655457L;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
public OrderProductId(Order order, Product product) {
this.order = order;
this.product = product;
}
public OrderProductId() {
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((order.getId() == null)
? 0
: order
.getId()
.hashCode());
result = prime * result + ((product.getId() == null)
? 0
: product
.getId()
.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
OrderProductId other = (OrderProductId) obj;
if (order == null) {
if (other.order != null) {
return false;
}
} else if (!order.equals(other.order)) {
return false;
}
if (product == null) {
if (other.product != null) {
return false;
}
} else if (!product.equals(other.product)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,40 @@
package com.baeldung.hexagonal.store.core.context.order.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
private String name;
private Double price;
public Product() {
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.hexagonal.store.core.context.order.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException() {
super("Product not found!");
}
}

View File

@@ -0,0 +1,5 @@
package com.baeldung.hexagonal.store.core.context.order.infrastructure;
public interface EmailNotificationSender {
void sendEmailMessage(String targetEmail, String subject, String body);
}

View File

@@ -0,0 +1,11 @@
package com.baeldung.hexagonal.store.core.context.order.infrastructure;
import com.baeldung.hexagonal.store.core.context.order.entity.Order;
import java.util.Optional;
public interface OrderDataStore {
Order save(Order orderProduct);
Optional<Order> findById(Long orderId);
Iterable<Order> findAll();
}

View File

@@ -0,0 +1,12 @@
package com.baeldung.hexagonal.store.core.context.order.infrastructure;
import com.baeldung.hexagonal.store.core.context.order.entity.OrderProductId;
import com.baeldung.hexagonal.store.core.context.order.entity.OrderProduct;
import java.util.Optional;
public interface OrderProductDataStore {
OrderProduct save(OrderProduct orderProduct);
Optional<OrderProduct> findById(OrderProductId orderProductPkId);
}

View File

@@ -0,0 +1,11 @@
package com.baeldung.hexagonal.store.core.context.order.infrastructure;
import com.baeldung.hexagonal.store.core.context.order.entity.Product;
import java.util.Optional;
public interface ProductDataStore {
Product save(Product product);
Optional<Product> findById(Long productId);
}

View File

@@ -0,0 +1,17 @@
package com.baeldung.hexagonal.store.core.context.order.service;
import com.baeldung.hexagonal.store.core.context.customer.exception.CustomerNotFoundException;
import com.baeldung.hexagonal.store.core.context.customer.exception.NotEnoughFundsException;
import com.baeldung.hexagonal.store.core.context.order.exception.ProductNotFoundException;
import com.baeldung.hexagonal.store.core.context.order.entity.Order;
import java.util.Map;
import java.util.Optional;
public interface OrderService {
Iterable<Order> getAllOrders();
Optional<Order> getOrderById(int id);
Optional<Order> processNewCustomerOrder(long customerId, Map<Long, Integer> productQuantityMap) throws CustomerNotFoundException, ProductNotFoundException, NotEnoughFundsException;
}

View File

@@ -0,0 +1,84 @@
package com.baeldung.hexagonal.store.core.context.order.service;
import com.baeldung.hexagonal.store.core.context.customer.entity.Customer;
import com.baeldung.hexagonal.store.core.context.customer.exception.CustomerNotFoundException;
import com.baeldung.hexagonal.store.core.context.customer.exception.NotEnoughFundsException;
import com.baeldung.hexagonal.store.core.context.customer.infrastructure.CustomerDataStore;
import com.baeldung.hexagonal.store.core.context.order.infrastructure.EmailNotificationSender;
import com.baeldung.hexagonal.store.core.context.order.infrastructure.OrderDataStore;
import com.baeldung.hexagonal.store.core.context.order.entity.Order;
import com.baeldung.hexagonal.store.core.context.order.entity.OrderProduct;
import com.baeldung.hexagonal.store.core.context.order.entity.Product;
import com.baeldung.hexagonal.store.core.context.order.exception.ProductNotFoundException;
import com.baeldung.hexagonal.store.core.context.order.infrastructure.ProductDataStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
public class OrderServiceImpl implements OrderService {
private CustomerDataStore customerDataStore;
private OrderDataStore orderDataStore;
private ProductDataStore productDataStore;
private EmailNotificationSender emailNotificationSender;
@Autowired
public OrderServiceImpl(CustomerDataStore customerDataStore, OrderDataStore orderDataStore, ProductDataStore productDataStore, EmailNotificationSender emailNotificationSender) {
this.customerDataStore = customerDataStore;
this.orderDataStore = orderDataStore;
this.productDataStore = productDataStore;
this.emailNotificationSender = emailNotificationSender;
}
@Override
public Iterable<Order> getAllOrders() {
return orderDataStore.findAll();
}
@Override
public Optional<Order> getOrderById(int id) {
return this.orderDataStore.findById((long) id);
}
@Override
public Optional<Order> processNewCustomerOrder(long customerId, Map<Long, Integer> productQuantityMap)
throws CustomerNotFoundException, ProductNotFoundException, NotEnoughFundsException {
Optional<Customer> customer = this.customerDataStore.findById(customerId);
if (!customer.isPresent()) {
throw new CustomerNotFoundException();
}
Customer existingCustomer = customer.get();
Order order = new Order();
List<OrderProduct> orderProducts = new ArrayList<>();
for (Map.Entry<Long, Integer> productEntry : productQuantityMap.entrySet()) {
Optional<Product> product = productDataStore.findById(productEntry.getKey());
if (!product.isPresent()) {
throw new ProductNotFoundException();
}
OrderProduct orderProduct = new OrderProduct(order, product.get(), productEntry.getValue());
orderProducts.add(orderProduct);
}
order.setOrderProducts(orderProducts);
Double finalPrice = order.getTotalOrderPrice();
if (!existingCustomer.hasEnoughFunds(finalPrice) && !existingCustomer.isNegativeBalanceAllowed()) {
throw new NotEnoughFundsException();
}
existingCustomer.withdrawFunds(finalPrice);
order.setStatus("Completed");
order.setCustomer(existingCustomer);
this.orderDataStore.save(order);
this.notifyCustomerAboutNewOrder(existingCustomer, order);
return Optional.of(order);
}
private void notifyCustomerAboutNewOrder(Customer customer, Order order) {
this.emailNotificationSender.sendEmailMessage(
customer.getEmail(),
"New order " + order.getId() + " was created in the Baeldung store",
"Order total is: " + order.getTotalOrderPrice());
}
}

View File

@@ -0,0 +1,53 @@
package com.baeldung.hexagonal.store.core;
import com.baeldung.hexagonal.store.core.context.customer.infrastructure.CustomerDataStore;
import com.baeldung.hexagonal.store.core.context.order.entity.Order;
import com.baeldung.hexagonal.store.core.context.order.infrastructure.EmailNotificationSender;
import com.baeldung.hexagonal.store.core.context.order.infrastructure.OrderDataStore;
import com.baeldung.hexagonal.store.core.context.order.infrastructure.ProductDataStore;
import com.baeldung.hexagonal.store.core.context.order.service.OrderServiceImpl;
import org.junit.Test;
import java.util.Collections;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MockDataStoreUnitTest {
@Test
public void givenMockedRepo_whenCalledGetAll_thenReturnMockedData() {
// Arrange
EmailNotificationSender emailSender = mock(EmailNotificationSender.class);
OrderDataStore orderDataStore = mock(OrderDataStore.class);
CustomerDataStore customerDataStore = mock(CustomerDataStore.class);
ProductDataStore productDataStore = mock(ProductDataStore.class);
OrderServiceImpl orderService = new OrderServiceImpl(customerDataStore, orderDataStore, productDataStore, emailSender);
long orderId = 2323L;
String status = "Pending";
Order order = createFakeOrder(orderId, status);
when(orderDataStore.findAll()).thenReturn(Collections.singletonList(order));
// Act
Iterable<Order> orders = orderService.getAllOrders();
// Assert
assertTrue(orders.iterator().hasNext());
Order firstItem = orders.iterator().next();
assertNotNull(firstItem);
assertEquals(orderId, (long) firstItem.getId());
assertEquals(status, firstItem.getStatus());
}
private static Order createFakeOrder(long id, String status) {
Order order = new Order();
order.setId(id);
order.setStatus(status);
return order;
}
}