Skip to content

Commit

Permalink
Add model classes
Browse files Browse the repository at this point in the history
  • Loading branch information
SvenWoltmann committed Jun 1, 2023
1 parent 102fab5 commit f423feb
Show file tree
Hide file tree
Showing 15 changed files with 503 additions and 0 deletions.
47 changes: 47 additions & 0 deletions model/src/main/java/eu/happycoders/shop/model/cart/Cart.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package eu.happycoders.shop.model.cart;

import eu.happycoders.shop.model.customer.CustomerId;
import eu.happycoders.shop.model.money.Money;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class Cart {

private final CustomerId id; // cart ID = customer ID

private final Map<ProductId, CartLineItem> lineItems = new LinkedHashMap<>();

public Cart(CustomerId id) {
this.id = id;
}

public void addProduct(Product product, int quantity) throws NotEnoughItemsInStockException {
lineItems
.computeIfAbsent(product.id(), __ -> new CartLineItem(product))
.increaseQuantityBy(quantity, product.itemsInStock());
}

// Use only when loading a Cart from the database
public void putProductIgnoringNotEnoughItemsInStock(Product product, int quantity) {
lineItems.put(product.id(), new CartLineItem(product, quantity));
}

public CustomerId id() {
return id;
}

public List<CartLineItem> lineItems() {
return List.copyOf(lineItems.values());
}

public int numberOfItems() {
return lineItems.values().stream().mapToInt(CartLineItem::quantity).sum();
}

public Money subTotal() {
return lineItems.values().stream().map(CartLineItem::subTotal).reduce(Money::add).orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package eu.happycoders.shop.model.cart;

import eu.happycoders.shop.model.money.Money;
import eu.happycoders.shop.model.product.Product;

public class CartLineItem {

private final Product product;
private int quantity;

public CartLineItem(Product product) {
this.product = product;
}

CartLineItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}

public Product product() {
return product;
}

public int quantity() {
return quantity;
}

public void increaseQuantityBy(int augend, int itemsInStock)
throws NotEnoughItemsInStockException {
int newQuantity = quantity + augend;
if (itemsInStock < newQuantity) {
throw new NotEnoughItemsInStockException(
"Product %s has less items in stock (%d) than the requested total quantity (%d)"
.formatted(product.id(), product.itemsInStock(), newQuantity),
product.itemsInStock());
}

this.quantity = newQuantity;
}

public Money subTotal() {
return product.price().multiply(quantity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package eu.happycoders.shop.model.cart;

public class NotEnoughItemsInStockException extends Exception {

private final int itemsInStock;

public NotEnoughItemsInStockException(String message, int itemsInStock) {
super(message);
this.itemsInStock = itemsInStock;
}

public int itemsInStock() {
return itemsInStock;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package eu.happycoders.shop.model.customer;

public record CustomerId(int value) {

public CustomerId {
if (value < 1) {
throw new IllegalArgumentException("'value' must be a positive integer");
}
}
}
48 changes: 48 additions & 0 deletions model/src/main/java/eu/happycoders/shop/model/money/Money.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package eu.happycoders.shop.model.money;

import java.math.BigDecimal;
import java.util.Currency;
import java.util.Objects;

public record Money(Currency currency, BigDecimal amount) {

public static final Currency EUR = Currency.getInstance("EUR");

public Money {
Objects.requireNonNull(currency, "'currency' must not be null");
Objects.requireNonNull(amount, "'amount' must not be null");
if (amount.scale() > currency.getDefaultFractionDigits()) {
throw new IllegalArgumentException(
"Scale of amount %s is greater than the number of fraction digits used with currency %s"
.formatted(amount, currency));
}
}

public static Money of(Currency currency, int mayor, int minor) {
int scale = currency.getDefaultFractionDigits();
return new Money(currency, BigDecimal.valueOf(mayor).add(BigDecimal.valueOf(minor, scale)));
}

public static Money ofMinor(Currency currency, int minor) {
int scale = currency.getDefaultFractionDigits();
return new Money(currency, BigDecimal.valueOf(minor, scale));
}

public int amountInMinorUnit() {
return amount.scaleByPowerOfTen(amount.scale()).intValue();
}

public Money multiply(int multiplicand) {
return new Money(currency, amount.multiply(BigDecimal.valueOf(multiplicand)));
}

public Money add(Money augend) {
if (!this.currency.equals(augend.currency())) {
throw new IllegalArgumentException(
"Currency %s of augend does not match this money's currency %s"
.formatted(augend.currency(), this.currency));
}

return new Money(currency, amount.add(augend.amount()));
}
}
46 changes: 46 additions & 0 deletions model/src/main/java/eu/happycoders/shop/model/product/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package eu.happycoders.shop.model.product;

import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException;
import eu.happycoders.shop.model.money.Money;

public class Product {

private final ProductId id;
private String name;
private String description;
private Money price;
private int itemsInStock;

public Product(ProductId id, String name, String description, Money price, int itemsInStock) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.itemsInStock = itemsInStock;
}

public ProductId id() {
return id;
}

public void addToCart(Cart cart, int quantity) throws NotEnoughItemsInStockException {
cart.addProduct(this, quantity);
}

public String name() {
return name;
}

public String description() {
return description;
}

public Money price() {
return price;
}

public int itemsInStock() {
return itemsInStock;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package eu.happycoders.shop.model.product;

import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

public record ProductId(String value) {

private static final String ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static final int LENGTH_OF_NEW_PRODUCT_IDS = 8;

public ProductId {
Objects.requireNonNull(value, "'value' must not be null");
if (value.isEmpty()) {
throw new IllegalArgumentException("'value' must not be empty");
}
}

public static ProductId randomProductId() {
ThreadLocalRandom random = ThreadLocalRandom.current();
char[] chars = new char[LENGTH_OF_NEW_PRODUCT_IDS];
for (int i = 0; i < LENGTH_OF_NEW_PRODUCT_IDS; i++) {
chars[i] = ALPHABET.charAt(random.nextInt(ALPHABET.length()));
}
return new ProductId(new String(chars));
}
}
69 changes: 69 additions & 0 deletions model/src/test/java/eu/happycoders/shop/model/cart/CartTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package eu.happycoders.shop.model.cart;

import static eu.happycoders.shop.model.cart.TestCartFactory.emptyCartForRandomCustomer;
import static eu.happycoders.shop.model.money.TestMoneyFactory.euros;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;

import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.TestProductFactory;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test;

class CartTest {

@Test
void givenEmptyCart_addTwoProducts_productsAreInCart() throws NotEnoughItemsInStockException {
Cart cart = emptyCartForRandomCustomer();

Product product1 = TestProductFactory.createTestProduct(euros(12, 99));
Product product2 = TestProductFactory.createTestProduct(euros(5, 97));

cart.addProduct(product1, 3);
cart.addProduct(product2, 5);

assertThat(cart.lineItems()).hasSize(2);
assertThat(cart.lineItems().get(0).product()).isEqualTo(product1);
assertThat(cart.lineItems().get(0).quantity()).isEqualTo(3);
assertThat(cart.lineItems().get(1).product()).isEqualTo(product2);
assertThat(cart.lineItems().get(1).quantity()).isEqualTo(5);
}

@Test
void givenEmptyCart_addTwoProducts_numberOfItemsAndSubTotalIsCalculatedCorrectly()
throws NotEnoughItemsInStockException {
Cart cart = emptyCartForRandomCustomer();

Product product1 = TestProductFactory.createTestProduct(euros(12, 99));
Product product2 = TestProductFactory.createTestProduct(euros(5, 97));

cart.addProduct(product1, 3);
cart.addProduct(product2, 5);

assertThat(cart.numberOfItems()).isEqualTo(8);
assertThat(cart.subTotal()).isEqualTo(euros(68, 82));
}

@Test
void givenAProductWithAFewItemsAvailable_addMoreItemsThanAvailableToTheCart_throwsException() {
Cart cart = emptyCartForRandomCustomer();
Product product1 = TestProductFactory.createTestProduct(euros(9, 97), 3);

ThrowingCallable invocation = () -> cart.addProduct(product1, 4);

assertThatExceptionOfType(NotEnoughItemsInStockException.class)
.isThrownBy(invocation)
.satisfies(ex -> assertThat(ex.itemsInStock()).isEqualTo(product1.itemsInStock()));
}

@Test
void givenAProductWithAFewItemsAvailable_addAllAvailableItemsToTheCart_succeeds() {
Cart cart = emptyCartForRandomCustomer();
Product product1 = TestProductFactory.createTestProduct(euros(9, 97), 3);

ThrowingCallable invocation = () -> cart.addProduct(product1, 3);

assertThatNoException().isThrownBy(invocation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package eu.happycoders.shop.model.cart;

import eu.happycoders.shop.model.customer.CustomerId;
import java.util.concurrent.ThreadLocalRandom;

public class TestCartFactory {

public static Cart emptyCartForRandomCustomer() {
return new Cart(new CustomerId(ThreadLocalRandom.current().nextInt(1_000_000)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package eu.happycoders.shop.model.customer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class CustomerIdTest {
@ParameterizedTest
@ValueSource(ints = {-100, -1, 0})
void givenAValueLessThan1_newCustomerId_throwsException(int value) {
ThrowableAssert.ThrowingCallable invocation = () -> new CustomerId(value);

assertThatIllegalArgumentException().isThrownBy(invocation);
}

@ParameterizedTest
@ValueSource(ints = {1, 8_765, 2_000_000_000})
void givenAValueGreatThanOrEqualTo1_newCustomerId_succeeds(int value) {
CustomerId customerId = new CustomerId(value);

assertThat(customerId.value()).isEqualTo(value);
}
}
Loading

0 comments on commit f423feb

Please sign in to comment.