-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
102fab5
commit f423feb
Showing
15 changed files
with
503 additions
and
0 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
model/src/main/java/eu/happycoders/shop/model/cart/Cart.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
model/src/main/java/eu/happycoders/shop/model/cart/CartLineItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
model/src/main/java/eu/happycoders/shop/model/cart/NotEnoughItemsInStockException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
model/src/main/java/eu/happycoders/shop/model/customer/CustomerId.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
48
model/src/main/java/eu/happycoders/shop/model/money/Money.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
46
model/src/main/java/eu/happycoders/shop/model/product/Product.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
model/src/main/java/eu/happycoders/shop/model/product/ProductId.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
69
model/src/test/java/eu/happycoders/shop/model/cart/CartTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
model/src/test/java/eu/happycoders/shop/model/cart/TestCartFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
model/src/test/java/eu/happycoders/shop/model/customer/CustomerIdTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.