diff --git a/axon/pom.xml b/axon/pom.xml index f6c43c7cbd..f2cdc34fd1 100644 --- a/axon/pom.xml +++ b/axon/pom.xml @@ -53,7 +53,7 @@ - 4.1.2 + 4.4.7 \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/commandmodel/OrderAggregate.java b/axon/src/main/java/com/baeldung/axon/commandmodel/OrderAggregate.java deleted file mode 100644 index 4ef02e6b54..0000000000 --- a/axon/src/main/java/com/baeldung/axon/commandmodel/OrderAggregate.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.baeldung.axon.commandmodel; - -import static org.axonframework.modelling.command.AggregateLifecycle.apply; - -import org.axonframework.commandhandling.CommandHandler; -import org.axonframework.eventsourcing.EventSourcingHandler; -import org.axonframework.modelling.command.AggregateIdentifier; -import org.axonframework.spring.stereotype.Aggregate; - -import com.baeldung.axon.coreapi.commands.ConfirmOrderCommand; -import com.baeldung.axon.coreapi.commands.PlaceOrderCommand; -import com.baeldung.axon.coreapi.commands.ShipOrderCommand; -import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; -import com.baeldung.axon.coreapi.events.OrderPlacedEvent; -import com.baeldung.axon.coreapi.events.OrderShippedEvent; -import com.baeldung.axon.coreapi.exceptions.UnconfirmedOrderException; - -@Aggregate -public class OrderAggregate { - - @AggregateIdentifier - private String orderId; - private boolean orderConfirmed; - - @CommandHandler - public OrderAggregate(PlaceOrderCommand command) { - apply(new OrderPlacedEvent(command.getOrderId(), command.getProduct())); - } - - @CommandHandler - public void handle(ConfirmOrderCommand command) { - apply(new OrderConfirmedEvent(orderId)); - } - - @CommandHandler - public void handle(ShipOrderCommand command) { - if (!orderConfirmed) { - throw new UnconfirmedOrderException(); - } - - apply(new OrderShippedEvent(orderId)); - } - - @EventSourcingHandler - public void on(OrderPlacedEvent event) { - this.orderId = event.getOrderId(); - this.orderConfirmed = false; - } - - @EventSourcingHandler - public void on(OrderConfirmedEvent event) { - this.orderConfirmed = true; - } - - protected OrderAggregate() { - // Required by Axon to build a default Aggregate prior to Event Sourcing - } - -} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/commandmodel/order/OrderAggregate.java b/axon/src/main/java/com/baeldung/axon/commandmodel/order/OrderAggregate.java new file mode 100644 index 0000000000..97342bdb3a --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/commandmodel/order/OrderAggregate.java @@ -0,0 +1,98 @@ +package com.baeldung.axon.commandmodel.order; + +import com.baeldung.axon.coreapi.commands.AddProductCommand; +import com.baeldung.axon.coreapi.commands.ConfirmOrderCommand; +import com.baeldung.axon.coreapi.commands.CreateOrderCommand; +import com.baeldung.axon.coreapi.commands.ShipOrderCommand; +import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; +import com.baeldung.axon.coreapi.events.OrderCreatedEvent; +import com.baeldung.axon.coreapi.events.OrderShippedEvent; +import com.baeldung.axon.coreapi.events.ProductAddedEvent; +import com.baeldung.axon.coreapi.events.ProductRemovedEvent; +import com.baeldung.axon.coreapi.exceptions.DuplicateOrderLineException; +import com.baeldung.axon.coreapi.exceptions.OrderAlreadyConfirmedException; +import com.baeldung.axon.coreapi.exceptions.UnconfirmedOrderException; +import org.axonframework.commandhandling.CommandHandler; +import org.axonframework.eventsourcing.EventSourcingHandler; +import org.axonframework.modelling.command.AggregateIdentifier; +import org.axonframework.modelling.command.AggregateMember; +import org.axonframework.spring.stereotype.Aggregate; + +import java.util.HashMap; +import java.util.Map; + +import static org.axonframework.modelling.command.AggregateLifecycle.apply; + +@Aggregate +public class OrderAggregate { + + @AggregateIdentifier + private String orderId; + private boolean orderConfirmed; + + @AggregateMember + private Map orderLines; + + @CommandHandler + public OrderAggregate(CreateOrderCommand command) { + apply(new OrderCreatedEvent(command.getOrderId())); + } + + @CommandHandler + public void handle(AddProductCommand command) { + if (orderConfirmed) { + throw new OrderAlreadyConfirmedException(orderId); + } + + String productId = command.getProductId(); + if (orderLines.containsKey(productId)) { + throw new DuplicateOrderLineException(productId); + } + apply(new ProductAddedEvent(orderId, productId)); + } + + @CommandHandler + public void handle(ConfirmOrderCommand command) { + if (orderConfirmed) { + return; + } + + apply(new OrderConfirmedEvent(orderId)); + } + + @CommandHandler + public void handle(ShipOrderCommand command) { + if (!orderConfirmed) { + throw new UnconfirmedOrderException(); + } + + apply(new OrderShippedEvent(orderId)); + } + + @EventSourcingHandler + public void on(OrderCreatedEvent event) { + this.orderId = event.getOrderId(); + this.orderConfirmed = false; + this.orderLines = new HashMap<>(); + } + + @EventSourcingHandler + public void on(OrderConfirmedEvent event) { + this.orderConfirmed = true; + } + + @EventSourcingHandler + public void on(ProductAddedEvent event) { + String productId = event.getProductId(); + this.orderLines.put(productId, new OrderLine(productId)); + } + + @EventSourcingHandler + public void on(ProductRemovedEvent event) { + this.orderLines.remove(event.getProductId()); + } + + protected OrderAggregate() { + // Required by Axon to build a default Aggregate prior to Event Sourcing + } +} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/commandmodel/order/OrderLine.java b/axon/src/main/java/com/baeldung/axon/commandmodel/order/OrderLine.java new file mode 100644 index 0000000000..e471ecbfe0 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/commandmodel/order/OrderLine.java @@ -0,0 +1,83 @@ +package com.baeldung.axon.commandmodel.order; + +import com.baeldung.axon.coreapi.commands.DecrementProductCountCommand; +import com.baeldung.axon.coreapi.commands.IncrementProductCountCommand; +import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; +import com.baeldung.axon.coreapi.events.ProductCountDecrementedEvent; +import com.baeldung.axon.coreapi.events.ProductCountIncrementedEvent; +import com.baeldung.axon.coreapi.events.ProductRemovedEvent; +import com.baeldung.axon.coreapi.exceptions.OrderAlreadyConfirmedException; +import org.axonframework.commandhandling.CommandHandler; +import org.axonframework.eventsourcing.EventSourcingHandler; +import org.axonframework.modelling.command.EntityId; + +import java.util.Objects; + +import static org.axonframework.modelling.command.AggregateLifecycle.apply; + +public class OrderLine { + + @EntityId + private final String productId; + private Integer count; + private boolean orderConfirmed; + + public OrderLine(String productId) { + this.productId = productId; + this.count = 1; + } + + @CommandHandler + public void handle(IncrementProductCountCommand command) { + if (orderConfirmed) { + throw new OrderAlreadyConfirmedException(command.getOrderId()); + } + + apply(new ProductCountIncrementedEvent(command.getOrderId(), productId)); + } + + @CommandHandler + public void handle(DecrementProductCountCommand command) { + if (orderConfirmed) { + throw new OrderAlreadyConfirmedException(command.getOrderId()); + } + + if (count <= 1) { + apply(new ProductRemovedEvent(command.getOrderId(), productId)); + } else { + apply(new ProductCountDecrementedEvent(command.getOrderId(), productId)); + } + } + + @EventSourcingHandler + public void on(ProductCountIncrementedEvent event) { + this.count++; + } + + @EventSourcingHandler + public void on(ProductCountDecrementedEvent event) { + this.count--; + } + + @EventSourcingHandler + public void on(OrderConfirmedEvent event) { + this.orderConfirmed = true; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OrderLine orderLine = (OrderLine) o; + return Objects.equals(productId, orderLine.productId) && Objects.equals(count, orderLine.count); + } + + @Override + public int hashCode() { + return Objects.hash(productId, count); + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/commands/AddProductCommand.java b/axon/src/main/java/com/baeldung/axon/coreapi/commands/AddProductCommand.java new file mode 100644 index 0000000000..28736aaadc --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/commands/AddProductCommand.java @@ -0,0 +1,50 @@ +package com.baeldung.axon.coreapi.commands; + +import org.axonframework.modelling.command.TargetAggregateIdentifier; + +import java.util.Objects; + +public class AddProductCommand { + + @TargetAggregateIdentifier + private final String orderId; + private final String productId; + + public AddProductCommand(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AddProductCommand that = (AddProductCommand) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "AddProductCommand{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/commands/CreateOrderCommand.java b/axon/src/main/java/com/baeldung/axon/coreapi/commands/CreateOrderCommand.java new file mode 100644 index 0000000000..ceb7fd6a08 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/commands/CreateOrderCommand.java @@ -0,0 +1,43 @@ +package com.baeldung.axon.coreapi.commands; + +import org.axonframework.modelling.command.TargetAggregateIdentifier; + +import java.util.Objects; + +public class CreateOrderCommand { + + @TargetAggregateIdentifier + private final String orderId; + + public CreateOrderCommand(String orderId) { + this.orderId = orderId; + } + + public String getOrderId() { + return orderId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CreateOrderCommand that = (CreateOrderCommand) o; + return Objects.equals(orderId, that.orderId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId); + } + + @Override + public String toString() { + return "CreateOrderCommand{" + + "orderId='" + orderId + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/commands/DecrementProductCountCommand.java b/axon/src/main/java/com/baeldung/axon/coreapi/commands/DecrementProductCountCommand.java new file mode 100644 index 0000000000..f6f4db00fc --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/commands/DecrementProductCountCommand.java @@ -0,0 +1,50 @@ +package com.baeldung.axon.coreapi.commands; + +import org.axonframework.modelling.command.TargetAggregateIdentifier; + +import java.util.Objects; + +public class DecrementProductCountCommand { + + @TargetAggregateIdentifier + private final String orderId; + private final String productId; + + public DecrementProductCountCommand(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DecrementProductCountCommand that = (DecrementProductCountCommand) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "DecrementProductCountCommand{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/commands/IncrementProductCountCommand.java b/axon/src/main/java/com/baeldung/axon/coreapi/commands/IncrementProductCountCommand.java new file mode 100644 index 0000000000..548faabe37 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/commands/IncrementProductCountCommand.java @@ -0,0 +1,50 @@ +package com.baeldung.axon.coreapi.commands; + +import org.axonframework.modelling.command.TargetAggregateIdentifier; + +import java.util.Objects; + +public class IncrementProductCountCommand { + + @TargetAggregateIdentifier + private final String orderId; + private final String productId; + + public IncrementProductCountCommand(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IncrementProductCountCommand that = (IncrementProductCountCommand) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "IncrementProductCountCommand{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/commands/PlaceOrderCommand.java b/axon/src/main/java/com/baeldung/axon/coreapi/commands/PlaceOrderCommand.java deleted file mode 100644 index c70d503050..0000000000 --- a/axon/src/main/java/com/baeldung/axon/coreapi/commands/PlaceOrderCommand.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.baeldung.axon.coreapi.commands; - -import java.util.Objects; - -import org.axonframework.modelling.command.TargetAggregateIdentifier; - -public class PlaceOrderCommand { - - @TargetAggregateIdentifier - private final String orderId; - private final String product; - - public PlaceOrderCommand(String orderId, String product) { - this.orderId = orderId; - this.product = product; - } - - public String getOrderId() { - return orderId; - } - - public String getProduct() { - return product; - } - - @Override - public int hashCode() { - return Objects.hash(orderId, product); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final PlaceOrderCommand other = (PlaceOrderCommand) obj; - return Objects.equals(this.orderId, other.orderId) - && Objects.equals(this.product, other.product); - } - - @Override - public String toString() { - return "PlaceOrderCommand{" + - "orderId='" + orderId + '\'' + - ", product='" + product + '\'' + - '}'; - } -} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/events/OrderCreatedEvent.java b/axon/src/main/java/com/baeldung/axon/coreapi/events/OrderCreatedEvent.java new file mode 100644 index 0000000000..5d2d8b7f55 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/events/OrderCreatedEvent.java @@ -0,0 +1,40 @@ +package com.baeldung.axon.coreapi.events; + +import java.util.Objects; + +public class OrderCreatedEvent { + + private final String orderId; + + public OrderCreatedEvent(String orderId) { + this.orderId = orderId; + } + + public String getOrderId() { + return orderId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OrderCreatedEvent that = (OrderCreatedEvent) o; + return Objects.equals(orderId, that.orderId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId); + } + + @Override + public String toString() { + return "OrderCreatedEvent{" + + "orderId='" + orderId + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/events/OrderPlacedEvent.java b/axon/src/main/java/com/baeldung/axon/coreapi/events/OrderPlacedEvent.java deleted file mode 100644 index 06de4c5f9f..0000000000 --- a/axon/src/main/java/com/baeldung/axon/coreapi/events/OrderPlacedEvent.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.baeldung.axon.coreapi.events; - -import java.util.Objects; - -public class OrderPlacedEvent { - - private final String orderId; - private final String product; - - public OrderPlacedEvent(String orderId, String product) { - this.orderId = orderId; - this.product = product; - } - - public String getOrderId() { - return orderId; - } - - public String getProduct() { - return product; - } - - @Override - public int hashCode() { - return Objects.hash(orderId, product); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final OrderPlacedEvent other = (OrderPlacedEvent) obj; - return Objects.equals(this.orderId, other.orderId) - && Objects.equals(this.product, other.product); - } - - @Override - public String toString() { - return "OrderPlacedEvent{" + - "orderId='" + orderId + '\'' + - ", product='" + product + '\'' + - '}'; - } -} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductAddedEvent.java b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductAddedEvent.java new file mode 100644 index 0000000000..091ef2a570 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductAddedEvent.java @@ -0,0 +1,47 @@ +package com.baeldung.axon.coreapi.events; + +import java.util.Objects; + +public class ProductAddedEvent { + + private final String orderId; + private final String productId; + + public ProductAddedEvent(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProductAddedEvent that = (ProductAddedEvent) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "ProductAddedEvent{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductCountDecrementedEvent.java b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductCountDecrementedEvent.java new file mode 100644 index 0000000000..4017916791 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductCountDecrementedEvent.java @@ -0,0 +1,47 @@ +package com.baeldung.axon.coreapi.events; + +import java.util.Objects; + +public class ProductCountDecrementedEvent { + + private final String orderId; + private final String productId; + + public ProductCountDecrementedEvent(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProductCountDecrementedEvent that = (ProductCountDecrementedEvent) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "ProductCountDecrementedEvent{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductCountIncrementedEvent.java b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductCountIncrementedEvent.java new file mode 100644 index 0000000000..2910a9ea6f --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductCountIncrementedEvent.java @@ -0,0 +1,47 @@ +package com.baeldung.axon.coreapi.events; + +import java.util.Objects; + +public class ProductCountIncrementedEvent { + + private final String orderId; + private final String productId; + + public ProductCountIncrementedEvent(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProductCountIncrementedEvent that = (ProductCountIncrementedEvent) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "ProductCountIncrementedEvent{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductRemovedEvent.java b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductRemovedEvent.java new file mode 100644 index 0000000000..7f89ccd1cc --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/events/ProductRemovedEvent.java @@ -0,0 +1,47 @@ +package com.baeldung.axon.coreapi.events; + +import java.util.Objects; + +public class ProductRemovedEvent { + + private final String orderId; + private final String productId; + + public ProductRemovedEvent(String orderId, String productId) { + this.orderId = orderId; + this.productId = productId; + } + + public String getOrderId() { + return orderId; + } + + public String getProductId() { + return productId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProductRemovedEvent that = (ProductRemovedEvent) o; + return Objects.equals(orderId, that.orderId) && Objects.equals(productId, that.productId); + } + + @Override + public int hashCode() { + return Objects.hash(orderId, productId); + } + + @Override + public String toString() { + return "ProductRemovedEvent{" + + "orderId='" + orderId + '\'' + + ", productId='" + productId + '\'' + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/exceptions/DuplicateOrderLineException.java b/axon/src/main/java/com/baeldung/axon/coreapi/exceptions/DuplicateOrderLineException.java new file mode 100644 index 0000000000..c8a62a6cf0 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/exceptions/DuplicateOrderLineException.java @@ -0,0 +1,8 @@ +package com.baeldung.axon.coreapi.exceptions; + +public class DuplicateOrderLineException extends IllegalStateException { + + public DuplicateOrderLineException(String productId) { + super("Cannot duplicate order line for product identifier [" + productId + "]"); + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/exceptions/OrderAlreadyConfirmedException.java b/axon/src/main/java/com/baeldung/axon/coreapi/exceptions/OrderAlreadyConfirmedException.java new file mode 100644 index 0000000000..5a4d1cdaec --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/exceptions/OrderAlreadyConfirmedException.java @@ -0,0 +1,8 @@ +package com.baeldung.axon.coreapi.exceptions; + +public class OrderAlreadyConfirmedException extends IllegalStateException { + + public OrderAlreadyConfirmedException(String orderId) { + super("Cannot perform operation because order [" + orderId + "] is already confirmed."); + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/queries/Order.java b/axon/src/main/java/com/baeldung/axon/coreapi/queries/Order.java new file mode 100644 index 0000000000..1810a053d3 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/coreapi/queries/Order.java @@ -0,0 +1,83 @@ +package com.baeldung.axon.coreapi.queries; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Order { + + private final String orderId; + private final Map products; + private OrderStatus orderStatus; + + public Order(String orderId) { + this.orderId = orderId; + this.products = new HashMap<>(); + orderStatus = OrderStatus.CREATED; + } + + public String getOrderId() { + return orderId; + } + + public Map getProducts() { + return products; + } + + public OrderStatus getOrderStatus() { + return orderStatus; + } + + public void addProduct(String productId) { + products.putIfAbsent(productId, 1); + } + + public void incrementProductInstance(String productId) { + products.computeIfPresent(productId, (id, count) -> ++count); + } + + public void decrementProductInstance(String productId) { + products.computeIfPresent(productId, (id, count) -> --count); + } + + + public void removeProduct(String productId) { + products.remove(productId); + } + + public void setOrderConfirmed() { + this.orderStatus = OrderStatus.CONFIRMED; + } + + public void setOrderShipped() { + this.orderStatus = OrderStatus.SHIPPED; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Order that = (Order) o; + return Objects.equals(orderId, that.orderId) + && Objects.equals(products, that.products) + && orderStatus == that.orderStatus; + } + + @Override + public int hashCode() { + return Objects.hash(orderId, products, orderStatus); + } + + @Override + public String toString() { + return "Order{" + + "orderId='" + orderId + '\'' + + ", products=" + products + + ", orderStatus=" + orderStatus + + '}'; + } +} diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderStatus.java b/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderStatus.java index d215c5fc32..fc5da5d77e 100644 --- a/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderStatus.java +++ b/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderStatus.java @@ -2,6 +2,5 @@ package com.baeldung.axon.coreapi.queries; public enum OrderStatus { - PLACED, CONFIRMED, SHIPPED - + CREATED, CONFIRMED, SHIPPED } diff --git a/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderedProduct.java b/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderedProduct.java deleted file mode 100644 index d847bb2a98..0000000000 --- a/axon/src/main/java/com/baeldung/axon/coreapi/queries/OrderedProduct.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.baeldung.axon.coreapi.queries; - -import java.util.Objects; - -public class OrderedProduct { - - private final String orderId; - private final String product; - private OrderStatus orderStatus; - - public OrderedProduct(String orderId, String product) { - this.orderId = orderId; - this.product = product; - orderStatus = OrderStatus.PLACED; - } - - public String getOrderId() { - return orderId; - } - - public String getProduct() { - return product; - } - - public OrderStatus getOrderStatus() { - return orderStatus; - } - - public void setOrderConfirmed() { - this.orderStatus = OrderStatus.CONFIRMED; - } - - public void setOrderShipped() { - this.orderStatus = OrderStatus.SHIPPED; - } - - @Override - public int hashCode() { - return Objects.hash(orderId, product, orderStatus); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final OrderedProduct other = (OrderedProduct) obj; - return Objects.equals(this.orderId, other.orderId) - && Objects.equals(this.product, other.product) - && Objects.equals(this.orderStatus, other.orderStatus); - } - - @Override - public String toString() { - return "OrderedProduct{" + - "orderId='" + orderId + '\'' + - ", product='" + product + '\'' + - ", orderStatus=" + orderStatus + - '}'; - } -} diff --git a/axon/src/main/java/com/baeldung/axon/gui/OrderRestEndpoint.java b/axon/src/main/java/com/baeldung/axon/gui/OrderRestEndpoint.java index a9f34cc691..11e03bf6a5 100644 --- a/axon/src/main/java/com/baeldung/axon/gui/OrderRestEndpoint.java +++ b/axon/src/main/java/com/baeldung/axon/gui/OrderRestEndpoint.java @@ -1,20 +1,24 @@ package com.baeldung.axon.gui; -import java.util.List; -import java.util.UUID; - +import com.baeldung.axon.coreapi.commands.AddProductCommand; +import com.baeldung.axon.coreapi.commands.ConfirmOrderCommand; +import com.baeldung.axon.coreapi.commands.CreateOrderCommand; +import com.baeldung.axon.coreapi.commands.DecrementProductCountCommand; +import com.baeldung.axon.coreapi.commands.IncrementProductCountCommand; +import com.baeldung.axon.coreapi.commands.ShipOrderCommand; +import com.baeldung.axon.coreapi.queries.FindAllOrderedProductsQuery; +import com.baeldung.axon.coreapi.queries.Order; import org.axonframework.commandhandling.gateway.CommandGateway; import org.axonframework.messaging.responsetypes.ResponseTypes; import org.axonframework.queryhandling.QueryGateway; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import com.baeldung.axon.coreapi.commands.ConfirmOrderCommand; -import com.baeldung.axon.coreapi.commands.PlaceOrderCommand; -import com.baeldung.axon.coreapi.commands.ShipOrderCommand; -import com.baeldung.axon.coreapi.queries.FindAllOrderedProductsQuery; -import com.baeldung.axon.coreapi.queries.OrderedProduct; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; @RestController public class OrderRestEndpoint { @@ -28,25 +32,63 @@ public class OrderRestEndpoint { } @PostMapping("/ship-order") - public void shipOrder() { + public CompletableFuture shipOrder() { String orderId = UUID.randomUUID().toString(); - commandGateway.send(new PlaceOrderCommand(orderId, "Deluxe Chair")); - commandGateway.send(new ConfirmOrderCommand(orderId)); - commandGateway.send(new ShipOrderCommand(orderId)); + return commandGateway.send(new CreateOrderCommand(orderId)) + .thenCompose(result -> commandGateway.send(new AddProductCommand(orderId, "Deluxe Chair"))) + .thenCompose(result -> commandGateway.send(new ConfirmOrderCommand(orderId))) + .thenCompose(result -> commandGateway.send(new ShipOrderCommand(orderId))); } @PostMapping("/ship-unconfirmed-order") - public void shipUnconfirmedOrder() { + public CompletableFuture shipUnconfirmedOrder() { String orderId = UUID.randomUUID().toString(); - commandGateway.send(new PlaceOrderCommand(orderId, "Deluxe Chair")); - // This throws an exception, as an Order cannot be shipped if it has not been confirmed yet. - commandGateway.send(new ShipOrderCommand(orderId)); + return commandGateway.send(new CreateOrderCommand(orderId)) + .thenCompose(result -> commandGateway.send(new AddProductCommand(orderId, "Deluxe Chair"))) + // This throws an exception, as an Order cannot be shipped if it has not been confirmed yet. + .thenCompose(result -> commandGateway.send(new ShipOrderCommand(orderId))); + } + + @PostMapping("/order") + public CompletableFuture createOrder() { + return createOrder(UUID.randomUUID().toString()); + } + + @PostMapping("/order/{order-id}") + public CompletableFuture createOrder(@PathVariable("order-id") String orderId) { + return commandGateway.send(new CreateOrderCommand(orderId)); + } + + @PostMapping("/order/{order-id}/product/{product-id}") + public CompletableFuture addProduct(@PathVariable("order-id") String orderId, + @PathVariable("product-id") String productId) { + return commandGateway.send(new AddProductCommand(orderId, productId)); + } + + @PostMapping("/order/{order-id}/product/{product-id}/increment") + public CompletableFuture incrementProduct(@PathVariable("order-id") String orderId, + @PathVariable("product-id") String productId) { + return commandGateway.send(new IncrementProductCountCommand(orderId, productId)); + } + + @PostMapping("/order/{order-id}/product/{product-id}/decrement") + public CompletableFuture decrementProduct(@PathVariable("order-id") String orderId, + @PathVariable("product-id") String productId) { + return commandGateway.send(new DecrementProductCountCommand(orderId, productId)); + } + + @PostMapping("/order/{order-id}/confirm") + public CompletableFuture confirmOrder(@PathVariable("order-id") String orderId) { + return commandGateway.send(new ConfirmOrderCommand(orderId)); + } + + @PostMapping("/order/{order-id}/ship") + public CompletableFuture shipOrder(@PathVariable("order-id") String orderId) { + return commandGateway.send(new ShipOrderCommand(orderId)); } @GetMapping("/all-orders") - public List findAllOrderedProducts() { - return queryGateway.query(new FindAllOrderedProductsQuery(), ResponseTypes.multipleInstancesOf(OrderedProduct.class)) - .join(); + public CompletableFuture> findAllOrders() { + return queryGateway.query(new FindAllOrderedProductsQuery(), ResponseTypes.multipleInstancesOf(Order.class)); } - } diff --git a/axon/src/main/java/com/baeldung/axon/querymodel/OrderedProductsEventHandler.java b/axon/src/main/java/com/baeldung/axon/querymodel/OrderedProductsEventHandler.java deleted file mode 100644 index a37f0111ed..0000000000 --- a/axon/src/main/java/com/baeldung/axon/querymodel/OrderedProductsEventHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.baeldung.axon.querymodel; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.axonframework.config.ProcessingGroup; -import org.axonframework.eventhandling.EventHandler; -import org.axonframework.queryhandling.QueryHandler; -import org.springframework.stereotype.Service; - -import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; -import com.baeldung.axon.coreapi.events.OrderPlacedEvent; -import com.baeldung.axon.coreapi.events.OrderShippedEvent; -import com.baeldung.axon.coreapi.queries.FindAllOrderedProductsQuery; -import com.baeldung.axon.coreapi.queries.OrderedProduct; - -@Service -@ProcessingGroup("ordered-products") -public class OrderedProductsEventHandler { - - private final Map orderedProducts = new HashMap<>(); - - @EventHandler - public void on(OrderPlacedEvent event) { - String orderId = event.getOrderId(); - orderedProducts.put(orderId, new OrderedProduct(orderId, event.getProduct())); - } - - @EventHandler - public void on(OrderConfirmedEvent event) { - orderedProducts.computeIfPresent(event.getOrderId(), (orderId, orderedProduct) -> { - orderedProduct.setOrderConfirmed(); - return orderedProduct; - }); - } - - @EventHandler - public void on(OrderShippedEvent event) { - orderedProducts.computeIfPresent(event.getOrderId(), (orderId, orderedProduct) -> { - orderedProduct.setOrderShipped(); - return orderedProduct; - }); - } - - @QueryHandler - public List handle(FindAllOrderedProductsQuery query) { - return new ArrayList<>(orderedProducts.values()); - } - -} \ No newline at end of file diff --git a/axon/src/main/java/com/baeldung/axon/querymodel/OrdersEventHandler.java b/axon/src/main/java/com/baeldung/axon/querymodel/OrdersEventHandler.java new file mode 100644 index 0000000000..25666b0bf3 --- /dev/null +++ b/axon/src/main/java/com/baeldung/axon/querymodel/OrdersEventHandler.java @@ -0,0 +1,86 @@ +package com.baeldung.axon.querymodel; + +import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; +import com.baeldung.axon.coreapi.events.OrderCreatedEvent; +import com.baeldung.axon.coreapi.events.OrderShippedEvent; +import com.baeldung.axon.coreapi.events.ProductAddedEvent; +import com.baeldung.axon.coreapi.events.ProductCountDecrementedEvent; +import com.baeldung.axon.coreapi.events.ProductCountIncrementedEvent; +import com.baeldung.axon.coreapi.events.ProductRemovedEvent; +import com.baeldung.axon.coreapi.queries.FindAllOrderedProductsQuery; +import com.baeldung.axon.coreapi.queries.Order; +import org.axonframework.config.ProcessingGroup; +import org.axonframework.eventhandling.EventHandler; +import org.axonframework.queryhandling.QueryHandler; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@ProcessingGroup("orders") +public class OrdersEventHandler { + + private final Map orders = new HashMap<>(); + + @EventHandler + public void on(OrderCreatedEvent event) { + String orderId = event.getOrderId(); + orders.put(orderId, new Order(orderId)); + } + + @EventHandler + public void on(ProductAddedEvent event) { + orders.computeIfPresent(event.getOrderId(), (orderId, order) -> { + order.addProduct(event.getProductId()); + return order; + }); + } + + @EventHandler + public void on(ProductCountIncrementedEvent event) { + orders.computeIfPresent(event.getOrderId(), (orderId, order) -> { + order.incrementProductInstance(event.getProductId()); + return order; + }); + } + + @EventHandler + public void on(ProductCountDecrementedEvent event) { + orders.computeIfPresent(event.getOrderId(), (orderId, order) -> { + order.decrementProductInstance(event.getProductId()); + return order; + }); + } + + @EventHandler + public void on(ProductRemovedEvent event) { + orders.computeIfPresent(event.getOrderId(), (orderId, order) -> { + order.removeProduct(event.getProductId()); + return order; + }); + } + + @EventHandler + public void on(OrderConfirmedEvent event) { + orders.computeIfPresent(event.getOrderId(), (orderId, order) -> { + order.setOrderConfirmed(); + return order; + }); + } + + @EventHandler + public void on(OrderShippedEvent event) { + orders.computeIfPresent(event.getOrderId(), (orderId, order) -> { + order.setOrderShipped(); + return order; + }); + } + + @QueryHandler + public List handle(FindAllOrderedProductsQuery query) { + return new ArrayList<>(orders.values()); + } +} \ No newline at end of file diff --git a/axon/src/main/resources/order-api.http b/axon/src/main/resources/order-api.http index a3c69c72bc..6c06c48989 100644 --- a/axon/src/main/resources/order-api.http +++ b/axon/src/main/resources/order-api.http @@ -1,11 +1,37 @@ +### Create Order, Add Product, Confirm and Ship Order + POST http://localhost:8080/ship-order -### +### Create Order, Add Product and Ship Order POST http://localhost:8080/ship-unconfirmed-order -### +### Retrieve all existing Orders GET http://localhost:8080/all-orders +### Create Order with id 666a1661-474d-4046-8b12-8b5896312768 + +POST http://localhost:8080/order/666a1661-474d-4046-8b12-8b5896312768 + +### Add Product a6aa01eb-4e38-4dfb-b53b-b5b82961fbf3 to Order 666a1661-474d-4046-8b12-8b5896312768 + +POST http://localhost:8080/order/666a1661-474d-4046-8b12-8b5896312768/product/a6aa01eb-4e38-4dfb-b53b-b5b82961fbf3 + +### Increment Product a6aa01eb-4e38-4dfb-b53b-b5b82961fbf3 to Order 666a1661-474d-4046-8b12-8b5896312768 + +POST http://localhost:8080/order/666a1661-474d-4046-8b12-8b5896312768/product/a6aa01eb-4e38-4dfb-b53b-b5b82961fbf3/increment + +### Decrement Product a6aa01eb-4e38-4dfb-b53b-b5b82961fbf3 to Order 666a1661-474d-4046-8b12-8b5896312768 + +POST http://localhost:8080/order/666a1661-474d-4046-8b12-8b5896312768/product/a6aa01eb-4e38-4dfb-b53b-b5b82961fbf3/decrement + +### Confirm Order 666a1661-474d-4046-8b12-8b5896312768 + +POST http://localhost:8080/order/666a1661-474d-4046-8b12-8b5896312768/confirm + +### Ship Order 666a1661-474d-4046-8b12-8b5896312768 + +POST http://localhost:8080/order/666a1661-474d-4046-8b12-8b5896312768/ship + ### diff --git a/axon/src/test/java/com/baeldung/axon/commandmodel/OrderAggregateUnitTest.java b/axon/src/test/java/com/baeldung/axon/commandmodel/OrderAggregateUnitTest.java index aaefe49fb1..c1d6bdccc2 100644 --- a/axon/src/test/java/com/baeldung/axon/commandmodel/OrderAggregateUnitTest.java +++ b/axon/src/test/java/com/baeldung/axon/commandmodel/OrderAggregateUnitTest.java @@ -1,62 +1,139 @@ package com.baeldung.axon.commandmodel; -import java.util.UUID; - +import com.baeldung.axon.commandmodel.order.OrderAggregate; +import com.baeldung.axon.coreapi.commands.AddProductCommand; +import com.baeldung.axon.coreapi.commands.ConfirmOrderCommand; +import com.baeldung.axon.coreapi.commands.CreateOrderCommand; +import com.baeldung.axon.coreapi.commands.DecrementProductCountCommand; +import com.baeldung.axon.coreapi.commands.IncrementProductCountCommand; +import com.baeldung.axon.coreapi.commands.ShipOrderCommand; +import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; +import com.baeldung.axon.coreapi.events.OrderCreatedEvent; +import com.baeldung.axon.coreapi.events.OrderShippedEvent; +import com.baeldung.axon.coreapi.events.ProductAddedEvent; +import com.baeldung.axon.coreapi.events.ProductCountDecrementedEvent; +import com.baeldung.axon.coreapi.events.ProductCountIncrementedEvent; +import com.baeldung.axon.coreapi.events.ProductRemovedEvent; +import com.baeldung.axon.coreapi.exceptions.DuplicateOrderLineException; +import com.baeldung.axon.coreapi.exceptions.OrderAlreadyConfirmedException; import com.baeldung.axon.coreapi.exceptions.UnconfirmedOrderException; import org.axonframework.test.aggregate.AggregateTestFixture; import org.axonframework.test.aggregate.FixtureConfiguration; -import org.junit.*; +import org.axonframework.test.matchers.Matchers; +import org.junit.jupiter.api.*; -import com.baeldung.axon.coreapi.commands.ConfirmOrderCommand; -import com.baeldung.axon.coreapi.commands.PlaceOrderCommand; -import com.baeldung.axon.coreapi.commands.ShipOrderCommand; -import com.baeldung.axon.coreapi.events.OrderConfirmedEvent; -import com.baeldung.axon.coreapi.events.OrderPlacedEvent; -import com.baeldung.axon.coreapi.events.OrderShippedEvent; +import java.util.UUID; -public class OrderAggregateUnitTest { +class OrderAggregateUnitTest { + + private static final String ORDER_ID = UUID.randomUUID().toString(); + private static final String PRODUCT_ID = UUID.randomUUID().toString(); private FixtureConfiguration fixture; - @Before - public void setUp() { + @BeforeEach + void setUp() { fixture = new AggregateTestFixture<>(OrderAggregate.class); } @Test - public void giveNoPriorActivity_whenPlaceOrderCommand_thenShouldPublishOrderPlacedEvent() { - String orderId = UUID.randomUUID().toString(); - String product = "Deluxe Chair"; + void giveNoPriorActivity_whenCreateOrderCommand_thenShouldPublishOrderCreatedEvent() { fixture.givenNoPriorActivity() - .when(new PlaceOrderCommand(orderId, product)) - .expectEvents(new OrderPlacedEvent(orderId, product)); + .when(new CreateOrderCommand(ORDER_ID)) + .expectEvents(new OrderCreatedEvent(ORDER_ID)); } @Test - public void givenOrderPlacedEvent_whenConfirmOrderCommand_thenShouldPublishOrderConfirmedEvent() { - String orderId = UUID.randomUUID().toString(); - String product = "Deluxe Chair"; - fixture.given(new OrderPlacedEvent(orderId, product)) - .when(new ConfirmOrderCommand(orderId)) - .expectEvents(new OrderConfirmedEvent(orderId)); + void givenOrderCreatedEvent_whenAddProductCommand_thenShouldPublishProductAddedEvent() { + fixture.given(new OrderCreatedEvent(ORDER_ID)) + .when(new AddProductCommand(ORDER_ID, PRODUCT_ID)) + .expectEvents(new ProductAddedEvent(ORDER_ID, PRODUCT_ID)); } @Test - public void givenOrderPlacedEvent_whenShipOrderCommand_thenShouldThrowUnconfirmedOrderException() { - String orderId = UUID.randomUUID().toString(); - String product = "Deluxe Chair"; - fixture.given(new OrderPlacedEvent(orderId, product)) - .when(new ShipOrderCommand(orderId)) + void givenOrderCreatedEventAndProductAddedEvent_whenAddProductCommandForSameProductId_thenShouldThrowDuplicateOrderLineException() { + fixture.given(new OrderCreatedEvent(ORDER_ID), new ProductAddedEvent(ORDER_ID, PRODUCT_ID)) + .when(new AddProductCommand(ORDER_ID, PRODUCT_ID)) + .expectException(DuplicateOrderLineException.class) + .expectExceptionMessage(Matchers.predicate(message -> ((String) message).contains(PRODUCT_ID))); + } + + @Test + void givenOrderCreatedEventAndProductAddedEvent_whenIncrementProductCountCommand_thenShouldPublishProductCountIncrementedEvent() { + fixture.given(new OrderCreatedEvent(ORDER_ID), new ProductAddedEvent(ORDER_ID, PRODUCT_ID)) + .when(new IncrementProductCountCommand(ORDER_ID, PRODUCT_ID)) + .expectEvents(new ProductCountIncrementedEvent(ORDER_ID, PRODUCT_ID)); + } + + @Test + void givenOrderCreatedEventProductAddedEventAndProductCountIncrementedEvent_whenDecrementProductCountCommand_thenShouldPublishProductCountDecrementedEvent() { + fixture.given(new OrderCreatedEvent(ORDER_ID), + new ProductAddedEvent(ORDER_ID, PRODUCT_ID), + new ProductCountIncrementedEvent(ORDER_ID, PRODUCT_ID)) + .when(new DecrementProductCountCommand(ORDER_ID, PRODUCT_ID)) + .expectEvents(new ProductCountDecrementedEvent(ORDER_ID, PRODUCT_ID)); + } + + @Test + void givenOrderCreatedEventAndProductAddedEvent_whenDecrementProductCountCommand_thenShouldPublishProductRemovedEvent() { + fixture.given(new OrderCreatedEvent(ORDER_ID), new ProductAddedEvent(ORDER_ID, PRODUCT_ID)) + .when(new DecrementProductCountCommand(ORDER_ID, PRODUCT_ID)) + .expectEvents(new ProductRemovedEvent(ORDER_ID, PRODUCT_ID)); + } + + @Test + void givenOrderCreatedEvent_whenConfirmOrderCommand_thenShouldPublishOrderConfirmedEvent() { + fixture.given(new OrderCreatedEvent(ORDER_ID)) + .when(new ConfirmOrderCommand(ORDER_ID)) + .expectEvents(new OrderConfirmedEvent(ORDER_ID)); + } + + @Test + void givenOrderCreatedEventAndOrderConfirmedEvent_whenConfirmOrderCommand_thenExpectNoEvents() { + fixture.given(new OrderCreatedEvent(ORDER_ID), new OrderConfirmedEvent(ORDER_ID)) + .when(new ConfirmOrderCommand(ORDER_ID)) + .expectNoEvents(); + } + + @Test + void givenOrderCreatedEvent_whenShipOrderCommand_thenShouldThrowUnconfirmedOrderException() { + fixture.given(new OrderCreatedEvent(ORDER_ID)) + .when(new ShipOrderCommand(ORDER_ID)) .expectException(UnconfirmedOrderException.class); } @Test - public void givenOrderPlacedEventAndOrderConfirmedEvent_whenShipOrderCommand_thenShouldPublishOrderShippedEvent() { - String orderId = UUID.randomUUID().toString(); - String product = "Deluxe Chair"; - fixture.given(new OrderPlacedEvent(orderId, product), new OrderConfirmedEvent(orderId)) - .when(new ShipOrderCommand(orderId)) - .expectEvents(new OrderShippedEvent(orderId)); + void givenOrderCreatedEventAndOrderConfirmedEvent_whenShipOrderCommand_thenShouldPublishOrderShippedEvent() { + fixture.given(new OrderCreatedEvent(ORDER_ID), new OrderConfirmedEvent(ORDER_ID)) + .when(new ShipOrderCommand(ORDER_ID)) + .expectEvents(new OrderShippedEvent(ORDER_ID)); } + @Test + void givenOrderCreatedEventProductAndOrderConfirmedEvent_whenAddProductCommand_thenShouldThrowOrderAlreadyConfirmedException() { + fixture.given(new OrderCreatedEvent(ORDER_ID), new OrderConfirmedEvent(ORDER_ID)) + .when(new AddProductCommand(ORDER_ID, PRODUCT_ID)) + .expectException(OrderAlreadyConfirmedException.class) + .expectExceptionMessage(Matchers.predicate(message -> ((String) message).contains(ORDER_ID))); + } + + @Test + void givenOrderCreatedEventProductAddedEventAndOrderConfirmedEvent_whenIncrementProductCountCommand_thenShouldThrowOrderAlreadyConfirmedException() { + fixture.given(new OrderCreatedEvent(ORDER_ID), + new ProductAddedEvent(ORDER_ID, PRODUCT_ID), + new OrderConfirmedEvent(ORDER_ID)) + .when(new IncrementProductCountCommand(ORDER_ID, PRODUCT_ID)) + .expectException(OrderAlreadyConfirmedException.class) + .expectExceptionMessage(Matchers.predicate(message -> ((String) message).contains(ORDER_ID))); + } + + @Test + void givenOrderCreatedEventProductAddedEventAndOrderConfirmedEvent_whenDecrementProductCountCommand_thenShouldThrowOrderAlreadyConfirmedException() { + fixture.given(new OrderCreatedEvent(ORDER_ID), + new ProductAddedEvent(ORDER_ID, PRODUCT_ID), + new OrderConfirmedEvent(ORDER_ID)) + .when(new DecrementProductCountCommand(ORDER_ID, PRODUCT_ID)) + .expectException(OrderAlreadyConfirmedException.class) + .expectExceptionMessage(Matchers.predicate(message -> ((String) message).contains(ORDER_ID))); + } } \ No newline at end of file