diff --git a/order/order-web/src/main/java/demo/event/OrderEventType.java b/order/order-web/src/main/java/demo/event/OrderEventType.java
index 85305f2..38e79b5 100644
--- a/order/order-web/src/main/java/demo/event/OrderEventType.java
+++ b/order/order-web/src/main/java/demo/event/OrderEventType.java
@@ -10,6 +10,15 @@ import demo.order.OrderStatus;
* @author kbastani
*/
public enum OrderEventType {
- // TODO: Implement
- ORDER_CREATED
+ ORDER_CREATED,
+ ACCOUNT_CONNECTED,
+ RESERVATION_PENDING,
+ INVENTORY_RESERVED,
+ RESERVATION_SUCCEEDED,
+ RESERVATION_FAILED,
+ PAYMENT_CREATED,
+ PAYMENT_CONNECTED,
+ PAYMENT_PENDING,
+ PAYMENT_SUCCEEDED,
+ PAYMENT_FAILED
}
diff --git a/order/order-web/src/main/java/demo/order/Order.java b/order/order-web/src/main/java/demo/order/Order.java
index dca0545..dcb2b04 100644
--- a/order/order-web/src/main/java/demo/order/Order.java
+++ b/order/order-web/src/main/java/demo/order/Order.java
@@ -31,7 +31,7 @@ public class Order extends BaseEntity {
private Address shippingAddress;
public Order() {
- this.status = OrderStatus.PURCHASED;
+ this.status = OrderStatus.ORDER_CREATED;
}
public Order(String accountNumber, Address shippingAddress) {
diff --git a/order/order-web/src/main/java/demo/order/OrderCommand.java b/order/order-web/src/main/java/demo/order/OrderCommand.java
index f938f04..17ea5ce 100644
--- a/order/order-web/src/main/java/demo/order/OrderCommand.java
+++ b/order/order-web/src/main/java/demo/order/OrderCommand.java
@@ -1,6 +1,9 @@
package demo.order;
public enum OrderCommand {
- // TODO: Create commands
- TODO
+ CONNECT_ACCOUNT,
+ RESERVE_INVENTORY,
+ CREATE_PAYMENT,
+ CONNECT_PAYMENT,
+ PROCESS_PAYMENT
}
diff --git a/order/order-web/src/main/java/demo/order/OrderController.java b/order/order-web/src/main/java/demo/order/OrderController.java
index bf4b32d..2238d3e 100644
--- a/order/order-web/src/main/java/demo/order/OrderController.java
+++ b/order/order-web/src/main/java/demo/order/OrderController.java
@@ -76,10 +76,42 @@ public class OrderController {
.orElseThrow(() -> new RuntimeException("The order could not be found"));
}
- @GetMapping(path = "/orders/{id}/commands/todo")
- public ResponseEntity confirmOrder(@PathVariable Long id) {
+ @GetMapping(path = "/orders/{id}/commands/connectAccount")
+ public ResponseEntity connectAccount(@PathVariable Long id) {
return Optional.ofNullable(getOrderResource(
- orderService.applyCommand(id, OrderCommand.TODO)))
+ orderService.applyCommand(id, OrderCommand.CONNECT_ACCOUNT)))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The command could not be applied"));
+ }
+
+ @GetMapping(path = "/orders/{id}/commands/connectPayment")
+ public ResponseEntity connectPayment(@PathVariable Long id) {
+ return Optional.ofNullable(getOrderResource(
+ orderService.applyCommand(id, OrderCommand.CONNECT_PAYMENT)))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The command could not be applied"));
+ }
+
+ @GetMapping(path = "/orders/{id}/commands/createPayment")
+ public ResponseEntity createPayment(@PathVariable Long id) {
+ return Optional.ofNullable(getOrderResource(
+ orderService.applyCommand(id, OrderCommand.CREATE_PAYMENT)))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The command could not be applied"));
+ }
+
+ @GetMapping(path = "/orders/{id}/commands/processPayment")
+ public ResponseEntity processPayment(@PathVariable Long id) {
+ return Optional.ofNullable(getOrderResource(
+ orderService.applyCommand(id, OrderCommand.PROCESS_PAYMENT)))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The command could not be applied"));
+ }
+
+ @GetMapping(path = "/orders/{id}/commands/reserveInventory")
+ public ResponseEntity reserveInventory(@PathVariable Long id) {
+ return Optional.ofNullable(getOrderResource(
+ orderService.applyCommand(id, OrderCommand.RESERVE_INVENTORY)))
.map(e -> new ResponseEntity<>(e, HttpStatus.OK))
.orElseThrow(() -> new RuntimeException("The command could not be applied"));
}
@@ -177,17 +209,20 @@ public class OrderController {
if (orderResource != null) {
commandResource.add(
getCommandLinkBuilder(id)
- .slash("confirm")
- .withRel("confirm"),
+ .slash("connectAccount")
+ .withRel("connectAccount"),
getCommandLinkBuilder(id)
- .slash("activate")
- .withRel("activate"),
+ .slash("reserveInventory")
+ .withRel("reserveInventory"),
getCommandLinkBuilder(id)
- .slash("suspend")
- .withRel("suspend"),
+ .slash("createPayment")
+ .withRel("createPayment"),
getCommandLinkBuilder(id)
- .slash("archive")
- .withRel("archive")
+ .slash("connectPayment")
+ .withRel("connectPayment"),
+ getCommandLinkBuilder(id)
+ .slash("processPayment")
+ .withRel("processPayment")
);
}
diff --git a/order/order-web/src/main/java/demo/order/OrderStatus.java b/order/order-web/src/main/java/demo/order/OrderStatus.java
index 257cd01..1f8cd05 100644
--- a/order/order-web/src/main/java/demo/order/OrderStatus.java
+++ b/order/order-web/src/main/java/demo/order/OrderStatus.java
@@ -1,9 +1,15 @@
package demo.order;
public enum OrderStatus {
- PURCHASED,
- PENDING,
- CONFIRMED,
- SHIPPED,
- DELIVERED
+ ORDER_CREATED,
+ ACCOUNT_CONNECTED,
+ RESERVATION_PENDING,
+ INVENTORY_RESERVED,
+ RESERVATION_SUCCEEDED,
+ RESERVATION_FAILED,
+ PAYMENT_CREATED,
+ PAYMENT_CONNECTED,
+ PAYMENT_PENDING,
+ PAYMENT_SUCCEEDED,
+ PAYMENT_FAILED
}
diff --git a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java
index b2abd4f..fc68cd4 100644
--- a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java
+++ b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java
@@ -2,11 +2,15 @@ package demo.config;
import demo.event.OrderEvent;
import demo.event.OrderEventType;
-import demo.function.OrderFunction;
+import demo.function.*;
import demo.order.Order;
import demo.order.OrderStatus;
+import demo.stream.OrderStream;
import org.apache.log4j.Logger;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.hateoas.client.Traverson;
import org.springframework.statemachine.StateContext;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.action.Action;
@@ -15,6 +19,7 @@ import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
+import java.net.URI;
import java.util.EnumSet;
/**
@@ -23,7 +28,7 @@ import java.util.EnumSet;
*
* A state machine provides a robust declarative language for describing the state of an {@link Order}
* resource given a sequence of ordered {@link demo.event.OrderEvents}. When an event is received
- * in {@link demo.event.OrderEventStream}, an in-memory state machine is fully replicated given the
+ * in {@link OrderStream}, an in-memory state machine is fully replicated given the
* {@link demo.event.OrderEvents} attached to an {@link Order} resource.
*
* @author kbastani
@@ -44,7 +49,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter transitions) {
try {
// Describe state machine transitions for orders
- // TODO: Configure state machine
+ transitions.withExternal()
+ .source(OrderStatus.ORDER_CREATED)
+ .target(OrderStatus.ORDER_CREATED)
+ .event(OrderEventType.ORDER_CREATED)
+ .action(orderCreated())
+ .and()
+ .withExternal()
+ .source(OrderStatus.ORDER_CREATED)
+ .target(OrderStatus.ACCOUNT_CONNECTED)
+ .event(OrderEventType.ACCOUNT_CONNECTED)
+ .action(accountConnected())
+ .and()
+ .withExternal()
+ .source(OrderStatus.ACCOUNT_CONNECTED)
+ .target(OrderStatus.RESERVATION_PENDING)
+ .event(OrderEventType.RESERVATION_PENDING)
+ .action(reservationPending())
+ .and()
+ .withExternal()
+ .source(OrderStatus.RESERVATION_PENDING)
+ .target(OrderStatus.RESERVATION_SUCCEEDED)
+ .event(OrderEventType.RESERVATION_SUCCEEDED)
+ .action(reservationSucceeded())
+ .and()
+ .withExternal()
+ .source(OrderStatus.RESERVATION_PENDING)
+ .target(OrderStatus.RESERVATION_FAILED)
+ .event(OrderEventType.RESERVATION_FAILED)
+ .action(reservationFailed())
+ .and()
+ .withExternal()
+ .source(OrderStatus.RESERVATION_SUCCEEDED)
+ .target(OrderStatus.PAYMENT_CREATED)
+ .event(OrderEventType.PAYMENT_CREATED)
+ .action(paymentCreated())
+ .and()
+ .withExternal()
+ .source(OrderStatus.PAYMENT_CREATED)
+ .target(OrderStatus.PAYMENT_CONNECTED)
+ .event(OrderEventType.PAYMENT_CONNECTED)
+ .action(paymentConnected())
+ .and()
+ .withExternal()
+ .source(OrderStatus.PAYMENT_CONNECTED)
+ .target(OrderStatus.PAYMENT_PENDING)
+ .event(OrderEventType.PAYMENT_PENDING)
+ .action(paymentPending())
+ .and()
+ .withExternal()
+ .source(OrderStatus.PAYMENT_PENDING)
+ .target(OrderStatus.PAYMENT_SUCCEEDED)
+ .event(OrderEventType.PAYMENT_SUCCEEDED)
+ .action(paymentSucceeded())
+ .and()
+ .withExternal()
+ .source(OrderStatus.PAYMENT_PENDING)
+ .target(OrderStatus.PAYMENT_FAILED)
+ .event(OrderEventType.PAYMENT_FAILED)
+ .action(paymentFailed());
} catch (Exception e) {
throw new RuntimeException("Could not configure state machine transitions", e);
}
}
+ @Bean
+ public Action orderCreated() {
+ return context -> applyEvent(context,
+ new OrderCreated(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentPending() {
+ return context -> applyEvent(context,
+ new PaymentPending(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action reservationPending() {
+ return context -> applyEvent(context,
+ new ReservationPending(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentFailed() {
+ return context -> applyEvent(context,
+ new PaymentFailed(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentSucceeded() {
+ return context -> applyEvent(context,
+ new PaymentSucceeded(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentConnected() {
+ return context -> applyEvent(context,
+ new PaymentConnected(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentCreated() {
+ return context -> applyEvent(context,
+ new PaymentCreated(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action reservationSucceeded() {
+ return context -> applyEvent(context,
+ new ReservationSucceeded(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action reservationFailed() {
+ return context -> applyEvent(context,
+ new ReservationSucceeded(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action accountConnected() {
+ return context -> applyEvent(context,
+ new ReservationFailed(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("order").getHref());
+ // Get the account resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("order").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Order.class)
+ .getBody();
+ }));
+ }
+
/**
* Functions are mapped to actions that are triggered during the replication of a state machine. Functions
* should only be executed after the state machine has completed replication. This method checks the state
@@ -78,12 +311,12 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter context,
- OrderFunction orderFunction) {
+ OrderFunction orderFunction) {
OrderEvent orderEvent = null;
// Log out the progress of the state machine replication
diff --git a/order/order-worker/src/main/java/demo/event/OrderEventType.java b/order/order-worker/src/main/java/demo/event/OrderEventType.java
index 0984b05..1b31827 100644
--- a/order/order-worker/src/main/java/demo/event/OrderEventType.java
+++ b/order/order-worker/src/main/java/demo/event/OrderEventType.java
@@ -1,5 +1,15 @@
package demo.event;
public enum OrderEventType {
- // TODO: Add event types
+ ORDER_CREATED,
+ ACCOUNT_CONNECTED,
+ RESERVATION_PENDING,
+ INVENTORY_RESERVED,
+ RESERVATION_SUCCEEDED,
+ RESERVATION_FAILED,
+ PAYMENT_CREATED,
+ PAYMENT_CONNECTED,
+ PAYMENT_PENDING,
+ PAYMENT_SUCCEEDED,
+ PAYMENT_FAILED
}
diff --git a/order/order-worker/src/main/java/demo/function/AccountConnected.java b/order/order-worker/src/main/java/demo/function/AccountConnected.java
new file mode 100644
index 0000000..3fe3dc5
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/AccountConnected.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.order.Order;
+import demo.order.OrderStatus;
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class AccountConnected extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(AccountConnected.class);
+
+ public AccountConnected(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for account connected...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/OrderCreated.java b/order/order-worker/src/main/java/demo/function/OrderCreated.java
new file mode 100644
index 0000000..6c98ce3
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/OrderCreated.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class OrderCreated extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(OrderCreated.class);
+
+ public OrderCreated(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for order created...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/OrderFunction.java b/order/order-worker/src/main/java/demo/function/OrderFunction.java
index cb34c50..7214b53 100644
--- a/order/order-worker/src/main/java/demo/function/OrderFunction.java
+++ b/order/order-worker/src/main/java/demo/function/OrderFunction.java
@@ -46,6 +46,7 @@ public abstract class OrderFunction {
// Execute the lambda function
Order result = lambda.apply(event);
context.getExtendedState().getVariables().put("order", result);
+ log.info("Order function: " + event.getType());
return result;
}
}
diff --git a/order/order-worker/src/main/java/demo/function/PaymentConnected.java b/order/order-worker/src/main/java/demo/function/PaymentConnected.java
new file mode 100644
index 0000000..db1dace
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/PaymentConnected.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class PaymentConnected extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(PaymentConnected.class);
+
+ public PaymentConnected(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for payment connected...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/PaymentCreated.java b/order/order-worker/src/main/java/demo/function/PaymentCreated.java
new file mode 100644
index 0000000..a8743c4
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/PaymentCreated.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class PaymentCreated extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(PaymentCreated.class);
+
+ public PaymentCreated(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for payment created...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/PaymentFailed.java b/order/order-worker/src/main/java/demo/function/PaymentFailed.java
new file mode 100644
index 0000000..0227ca9
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/PaymentFailed.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class PaymentFailed extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(PaymentFailed.class);
+
+ public PaymentFailed(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for payment failed...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/PaymentPending.java b/order/order-worker/src/main/java/demo/function/PaymentPending.java
new file mode 100644
index 0000000..75e53cd
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/PaymentPending.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class PaymentPending extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(PaymentPending.class);
+
+ public PaymentPending(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for payment pending...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java b/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java
new file mode 100644
index 0000000..7f522b9
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class PaymentSucceeded extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(PaymentSucceeded.class);
+
+ public PaymentSucceeded(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for payment succeeded...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/ReservationFailed.java b/order/order-worker/src/main/java/demo/function/ReservationFailed.java
new file mode 100644
index 0000000..6933bce
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/ReservationFailed.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class ReservationFailed extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(ReservationFailed.class);
+
+ public ReservationFailed(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for reservation failed...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/ReservationPending.java b/order/order-worker/src/main/java/demo/function/ReservationPending.java
new file mode 100644
index 0000000..6e341b6
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/ReservationPending.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class ReservationPending extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(ReservationPending.class);
+
+ public ReservationPending(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for reservation pending...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java b/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java
new file mode 100644
index 0000000..7c9ce41
--- /dev/null
+++ b/order/order-worker/src/main/java/demo/function/ReservationSucceeded.java
@@ -0,0 +1,31 @@
+package demo.function;
+
+import demo.event.OrderEvent;
+import demo.event.OrderEventType;
+import demo.order.Order;
+import demo.order.OrderStatus;
+import org.apache.log4j.Logger;
+import org.springframework.statemachine.StateContext;
+
+import java.util.function.Function;
+
+public class ReservationSucceeded extends OrderFunction {
+
+ final private Logger log = Logger.getLogger(ReservationSucceeded.class);
+
+ public ReservationSucceeded(StateContext context, Function lambda) {
+ super(context, lambda);
+ }
+
+ /**
+ * Apply an {@link OrderEvent} to the lambda function that was provided through the
+ * constructor of this {@link OrderFunction}.
+ *
+ * @param event is the {@link OrderEvent} to apply to the lambda function
+ */
+ @Override
+ public Order apply(OrderEvent event) {
+ log.info("Executing workflow for reservation succeeded...");
+ return super.apply(event);
+ }
+}
diff --git a/order/order-worker/src/main/java/demo/order/LineItem.java b/order/order-worker/src/main/java/demo/order/LineItem.java
index f26f9db..02a26a4 100644
--- a/order/order-worker/src/main/java/demo/order/LineItem.java
+++ b/order/order-worker/src/main/java/demo/order/LineItem.java
@@ -1,17 +1,16 @@
package demo.order;
-/**
- * A simple domain class for the {@link LineItem} concept in the order context.
- *
- * @author Kenny Bastani
- * @author Josh Long
- */
-public class LineItem {
+import java.io.Serializable;
+
+public class LineItem implements Serializable {
private String name, productId;
private Integer quantity;
private Double price, tax;
+ public LineItem() {
+ }
+
public LineItem(String name, String productId, Integer quantity,
Double price, Double tax) {
this.name = name;
diff --git a/order/order-worker/src/main/java/demo/order/OrderStatus.java b/order/order-worker/src/main/java/demo/order/OrderStatus.java
index 7ffb9bb..b4f6230 100644
--- a/order/order-worker/src/main/java/demo/order/OrderStatus.java
+++ b/order/order-worker/src/main/java/demo/order/OrderStatus.java
@@ -1,14 +1,14 @@
package demo.order;
-/**
- * Describes the state of an {@link Order}.
- *
- * @author Kenny Bastani
- * @author Josh Long
- */
public enum OrderStatus {
- PENDING,
- CONFIRMED,
- SHIPPED,
- DELIVERED
+ ORDER_CREATED,
+ ACCOUNT_CONNECTED,
+ RESERVATION_PENDING,
+ RESERVATION_SUCCEEDED,
+ RESERVATION_FAILED,
+ PAYMENT_CREATED,
+ PAYMENT_CONNECTED,
+ PAYMENT_PENDING,
+ PAYMENT_SUCCEEDED,
+ PAYMENT_FAILED
}
diff --git a/order/order-worker/src/main/java/demo/event/OrderEventStream.java b/order/order-worker/src/main/java/demo/stream/OrderStream.java
similarity index 74%
rename from order/order-worker/src/main/java/demo/event/OrderEventStream.java
rename to order/order-worker/src/main/java/demo/stream/OrderStream.java
index cd2b471..1713e0d 100644
--- a/order/order-worker/src/main/java/demo/event/OrderEventStream.java
+++ b/order/order-worker/src/main/java/demo/stream/OrderStream.java
@@ -1,5 +1,7 @@
-package demo.event;
+package demo.stream;
+import demo.event.EventService;
+import demo.event.OrderEvent;
import demo.order.Order;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.annotation.EnableBinding;
@@ -8,7 +10,7 @@ import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.annotation.Profile;
/**
- * The {@link OrderEventStream} monitors for a variety of {@link OrderEvent} domain
+ * The {@link OrderStream} monitors for a variety of {@link OrderEvent} domain
* events for an {@link Order}.
*
* @author kbastani
@@ -16,11 +18,11 @@ import org.springframework.context.annotation.Profile;
@EnableAutoConfiguration
@EnableBinding(Sink.class)
@Profile({ "cloud", "development" })
-public class OrderEventStream {
+public class OrderStream {
private EventService eventService;
- public OrderEventStream(EventService eventService) {
+ public OrderStream(EventService eventService) {
this.eventService = eventService;
}
diff --git a/order/order-worker/src/main/resources/application.yml b/order/order-worker/src/main/resources/application.yml
index 37237e4..9f7428b 100644
--- a/order/order-worker/src/main/resources/application.yml
+++ b/order/order-worker/src/main/resources/application.yml
@@ -8,8 +8,8 @@ spring:
stream:
bindings:
input:
- destination: account
- group: account-group
+ destination: order
+ group: order-group
contentType: 'application/json'
consumer:
durableSubscription: true
diff --git a/payment/README.md b/payment/README.md
new file mode 100644
index 0000000..ac00b28
--- /dev/null
+++ b/payment/README.md
@@ -0,0 +1,3 @@
+# Payment Microservice
+
+This is the parent project that contains modules of a microservice deployment for the _Payment_ domain context. The two modules contained in this project are separated into separate deployment artifacts, one for synchronous HTTP-based interactions and one for asynchronous AMQP-based messaging.
diff --git a/payment/payment-web/pom.xml b/payment/payment-web/pom.xml
new file mode 100644
index 0000000..6b0f333
--- /dev/null
+++ b/payment/payment-web/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+ payment-web
+ 0.0.1-SNAPSHOT
+ jar
+
+ payment-web
+
+
+ org.kbastani
+ payment
+ 1.0-SNAPSHOT
+ ../
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-redis
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+
+ org.springframework.cloud
+ spring-cloud-starter-stream-rabbit
+
+
+ org.springframework.boot
+ spring-boot-starter-integration
+
+
+
+ com.h2database
+ h2
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/payment/payment-web/src/main/java/demo/PaymentServiceApplication.java b/payment/payment-web/src/main/java/demo/PaymentServiceApplication.java
new file mode 100644
index 0000000..3563fb0
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/PaymentServiceApplication.java
@@ -0,0 +1,14 @@
+package demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.hateoas.config.EnableHypermediaSupport;
+
+@SpringBootApplication
+@EnableHypermediaSupport(type = {EnableHypermediaSupport.HypermediaType.HAL})
+public class PaymentServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(PaymentServiceApplication.class, args);
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/config/CacheConfig.java b/payment/payment-web/src/main/java/demo/config/CacheConfig.java
new file mode 100644
index 0000000..0279460
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/config/CacheConfig.java
@@ -0,0 +1,46 @@
+package demo.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.Arrays;
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+ @Bean
+ public JedisConnectionFactory redisConnectionFactory(
+ @Value("${spring.redis.port}") Integer redisPort,
+ @Value("${spring.redis.host}") String redisHost) {
+ JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
+
+ redisConnectionFactory.setHostName(redisHost);
+ redisConnectionFactory.setPort(redisPort);
+
+ return redisConnectionFactory;
+ }
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory cf) {
+ RedisTemplate redisTemplate = new RedisTemplate();
+ redisTemplate.setConnectionFactory(cf);
+ return redisTemplate;
+ }
+
+ @Bean
+ public CacheManager cacheManager(RedisTemplate redisTemplate) {
+ RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
+ cacheManager.setDefaultExpiration(50000);
+ cacheManager.setCacheNames(Arrays.asList("payments", "payment-events"));
+ cacheManager.setUsePrefix(true);
+ return cacheManager;
+ }
+}
\ No newline at end of file
diff --git a/payment/payment-web/src/main/java/demo/config/JpaConfig.java b/payment/payment-web/src/main/java/demo/config/JpaConfig.java
new file mode 100644
index 0000000..c6c09e0
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/config/JpaConfig.java
@@ -0,0 +1,13 @@
+package demo.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+/**
+ * Enable JPA auditing on an empty configuration class to disable auditing on
+ *
+ */
+@Configuration
+@EnableJpaAuditing
+public class JpaConfig {
+}
diff --git a/payment/payment-web/src/main/java/demo/config/StreamConfig.java b/payment/payment-web/src/main/java/demo/config/StreamConfig.java
new file mode 100644
index 0000000..2eaf902
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/config/StreamConfig.java
@@ -0,0 +1,10 @@
+package demo.config;
+
+import org.springframework.cloud.stream.annotation.EnableBinding;
+import org.springframework.cloud.stream.messaging.Source;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@EnableBinding(Source.class)
+public class StreamConfig {
+}
diff --git a/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java b/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java
new file mode 100644
index 0000000..9282e23
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/config/WebMvcConfig.java
@@ -0,0 +1,36 @@
+package demo.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+import java.util.Collections;
+import java.util.List;
+
+@Configuration
+public class WebMvcConfig extends WebMvcConfigurerAdapter {
+
+ private ObjectMapper objectMapper;
+
+ public WebMvcConfig(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ public void configureMessageConverters(List> converters) {
+ final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
+ converter.setObjectMapper(objectMapper);
+ converters.add(converter);
+ }
+
+ @Bean
+ protected RestTemplate restTemplate(ObjectMapper objectMapper) {
+ MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
+ converter.setObjectMapper(objectMapper);
+ return new RestTemplate(Collections.singletonList(converter));
+ }
+}
\ No newline at end of file
diff --git a/payment/payment-web/src/main/java/demo/domain/BaseEntity.java b/payment/payment-web/src/main/java/demo/domain/BaseEntity.java
new file mode 100644
index 0000000..fb472ef
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/domain/BaseEntity.java
@@ -0,0 +1,48 @@
+package demo.domain;
+
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+import org.springframework.hateoas.ResourceSupport;
+
+import javax.persistence.EntityListeners;
+import javax.persistence.MappedSuperclass;
+import java.io.Serializable;
+
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public class BaseEntity extends ResourceSupport implements Serializable {
+
+ @CreatedDate
+ private Long createdAt;
+
+ @LastModifiedDate
+ private Long lastModified;
+
+ public BaseEntity() {
+ }
+
+ public Long getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Long createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public Long getLastModified() {
+ return lastModified;
+ }
+
+ public void setLastModified(Long lastModified) {
+ this.lastModified = lastModified;
+ }
+
+ @Override
+ public String toString() {
+ return "BaseEntity{" +
+ "createdAt=" + createdAt +
+ ", lastModified=" + lastModified +
+ '}';
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/event/ConsistencyModel.java b/payment/payment-web/src/main/java/demo/event/ConsistencyModel.java
new file mode 100644
index 0000000..8bef081
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/ConsistencyModel.java
@@ -0,0 +1,6 @@
+package demo.event;
+
+public enum ConsistencyModel {
+ BASE,
+ ACID
+}
diff --git a/payment/payment-web/src/main/java/demo/event/EventController.java b/payment/payment-web/src/main/java/demo/event/EventController.java
new file mode 100644
index 0000000..18e5e31
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/EventController.java
@@ -0,0 +1,39 @@
+package demo.event;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+
+@RestController
+@RequestMapping("/v1")
+public class EventController {
+
+ private final EventService eventService;
+
+ public EventController(EventService eventService) {
+ this.eventService = eventService;
+ }
+
+ @PostMapping(path = "/events/{id}")
+ public ResponseEntity createEvent(@RequestBody PaymentEvent event, @PathVariable Long id) {
+ return Optional.ofNullable(eventService.createEvent(id, event, ConsistencyModel.ACID))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
+ .orElseThrow(() -> new IllegalArgumentException("Event creation failed"));
+ }
+
+ @PutMapping(path = "/events/{id}")
+ public ResponseEntity updateEvent(@RequestBody PaymentEvent event, @PathVariable Long id) {
+ return Optional.ofNullable(eventService.updateEvent(id, event))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new IllegalArgumentException("Event update failed"));
+ }
+
+ @GetMapping(path = "/events/{id}")
+ public ResponseEntity getEvent(@PathVariable Long id) {
+ return Optional.ofNullable(eventService.getEvent(id))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/event/EventRepository.java b/payment/payment-web/src/main/java/demo/event/EventRepository.java
new file mode 100644
index 0000000..e906d2c
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/EventRepository.java
@@ -0,0 +1,10 @@
+package demo.event;
+
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.query.Param;
+
+public interface EventRepository extends JpaRepository {
+ Page findPaymentEventsByPaymentId(@Param("paymentId") Long paymentId, Pageable pageable);
+}
diff --git a/payment/payment-web/src/main/java/demo/event/EventService.java b/payment/payment-web/src/main/java/demo/event/EventService.java
new file mode 100644
index 0000000..b46e642
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/EventService.java
@@ -0,0 +1,214 @@
+package demo.event;
+
+import demo.payment.Payment;
+import demo.payment.PaymentController;
+import org.apache.log4j.Logger;
+import org.springframework.cache.annotation.CacheConfig;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cloud.stream.messaging.Source;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.hateoas.Resource;
+import org.springframework.http.RequestEntity;
+import org.springframework.integration.support.MessageBuilder;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
+
+/**
+ * The {@link EventService} provides transactional service methods for {@link PaymentEvent}
+ * entities of the Payment Service. Payment domain events are generated with a {@link PaymentEventType},
+ * and action logs are appended to the {@link PaymentEvent}.
+ *
+ * @author kbastani
+ */
+@Service
+@CacheConfig(cacheNames = {"payment-events"})
+public class EventService {
+
+ private final Logger log = Logger.getLogger(EventService.class);
+
+ private final EventRepository eventRepository;
+ private final Source paymentStreamSource;
+ private final RestTemplate restTemplate;
+
+ public EventService(EventRepository eventRepository, Source paymentStreamSource, RestTemplate restTemplate) {
+ this.eventRepository = eventRepository;
+ this.paymentStreamSource = paymentStreamSource;
+ this.restTemplate = restTemplate;
+ }
+
+ /**
+ * Create a new {@link PaymentEvent} and append it to the event log of the referenced {@link Payment}.
+ * After the {@link PaymentEvent} has been persisted, send the event to the payment stream. Events can
+ * be raised as a blocking or non-blocking operation depending on the {@link ConsistencyModel}.
+ *
+ * @param paymentId is the unique identifier for the {@link Payment}
+ * @param event is the {@link PaymentEvent} to create
+ * @param consistencyModel is the desired consistency model for the response
+ * @return an {@link PaymentEvent} that has been appended to the {@link Payment}'s event log
+ */
+ public PaymentEvent createEvent(Long paymentId, PaymentEvent event, ConsistencyModel consistencyModel) {
+ event = createEvent(paymentId, event);
+ return raiseEvent(event, consistencyModel);
+ }
+
+ /**
+ * Raise an {@link PaymentEvent} that attempts to transition the state of an {@link Payment}.
+ *
+ * @param event is an {@link PaymentEvent} that will be raised
+ * @param consistencyModel is the consistency model for this request
+ * @return an {@link PaymentEvent} that has been appended to the {@link Payment}'s event log
+ */
+ public PaymentEvent raiseEvent(PaymentEvent event, ConsistencyModel consistencyModel) {
+ switch (consistencyModel) {
+ case BASE:
+ asyncRaiseEvent(event);
+ break;
+ case ACID:
+ event = raiseEvent(event);
+ break;
+ }
+
+ return event;
+ }
+
+ /**
+ * Raise an asynchronous {@link PaymentEvent} by sending an AMQP message to the payment stream. Any
+ * state changes will be applied to the {@link Payment} outside of the current HTTP request context.
+ *
+ * Use this operation when a workflow can be processed asynchronously outside of the current HTTP
+ * request context.
+ *
+ * @param event is an {@link PaymentEvent} that will be raised
+ */
+ private void asyncRaiseEvent(PaymentEvent event) {
+ // Append the payment event to the stream
+ paymentStreamSource.output()
+ .send(MessageBuilder
+ .withPayload(getPaymentEventResource(event))
+ .build());
+ }
+
+ /**
+ * Raise a synchronous {@link PaymentEvent} by sending a HTTP request to the payment stream. The response
+ * is a blocking operation, which ensures that the result of a multi-step workflow will not return until
+ * the transaction reaches a consistent state.
+ *
+ * Use this operation when the result of a workflow must be returned within the current HTTP request context.
+ *
+ * @param event is an {@link PaymentEvent} that will be raised
+ * @return an {@link PaymentEvent} which contains the consistent state of an {@link Payment}
+ */
+ private PaymentEvent raiseEvent(PaymentEvent event) {
+ try {
+ // Create a new request entity
+ RequestEntity> requestEntity = RequestEntity.post(
+ URI.create("http://localhost:8081/v1/events"))
+ .contentType(MediaTypes.HAL_JSON)
+ .body(getPaymentEventResource(event), Resource.class);
+
+ // Update the payment entity's status
+ Payment result = restTemplate.exchange(requestEntity, Payment.class)
+ .getBody();
+
+ log.info(result);
+ event.setPayment(result);
+ } catch (Exception ex) {
+ log.error(ex);
+ }
+
+ return event;
+ }
+
+
+ /**
+ * Create a new {@link PaymentEvent} and publish it to the payment stream.
+ *
+ * @param event is the {@link PaymentEvent} to publish to the payment stream
+ * @return a hypermedia {@link PaymentEvent} resource
+ */
+ @CacheEvict(cacheNames = "payment-events", key = "#id.toString()")
+ public PaymentEvent createEvent(Long id, PaymentEvent event) {
+ // Save new event
+ event = addEvent(event);
+ Assert.notNull(event, "The event could not be appended to the payment");
+
+ return event;
+ }
+
+ /**
+ * Get an {@link PaymentEvent} with the supplied identifier.
+ *
+ * @param id is the unique identifier for the {@link PaymentEvent}
+ * @return an {@link PaymentEvent}
+ */
+ public Resource getEvent(Long id) {
+ return getPaymentEventResource(eventRepository.findOne(id));
+ }
+
+ /**
+ * Update an {@link PaymentEvent} with the supplied identifier.
+ *
+ * @param id is the unique identifier for the {@link PaymentEvent}
+ * @param event is the {@link PaymentEvent} to update
+ * @return the updated {@link PaymentEvent}
+ */
+ @CacheEvict(cacheNames = "payment-events", key = "#event.getPayment().getPaymentId().toString()")
+ public PaymentEvent updateEvent(Long id, PaymentEvent event) {
+ Assert.notNull(id);
+ Assert.isTrue(event.getId() == null || Objects.equals(id, event.getId()));
+
+ return eventRepository.save(event);
+ }
+
+ /**
+ * Get {@link PaymentEvents} for the supplied {@link Payment} identifier.
+ *
+ * @param id is the unique identifier of the {@link Payment}
+ * @return a list of {@link PaymentEvent} wrapped in a hypermedia {@link PaymentEvents} resource
+ */
+ @Cacheable(cacheNames = "payment-events", key = "#id.toString()")
+ public List getPaymentEvents(Long id) {
+ return eventRepository.findPaymentEventsByPaymentId(id,
+ new PageRequest(0, Integer.MAX_VALUE)).getContent();
+ }
+
+ /**
+ * Gets a hypermedia resource for a {@link PaymentEvent} entity.
+ *
+ * @param event is the {@link PaymentEvent} to enrich with hypermedia
+ * @return a hypermedia resource for the supplied {@link PaymentEvent} entity
+ */
+ private Resource getPaymentEventResource(PaymentEvent event) {
+ return new Resource(event, Arrays.asList(
+ linkTo(PaymentController.class)
+ .slash("events")
+ .slash(event.getEventId())
+ .withSelfRel(),
+ linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(event.getPayment().getPaymentId())
+ .withRel("payment")));
+ }
+
+ /**
+ * Add a {@link PaymentEvent} to an {@link Payment} entity.
+ *
+ * @param event is the {@link PaymentEvent} to append to an {@link Payment} entity
+ * @return the newly appended {@link PaymentEvent} entity
+ */
+ @CacheEvict(cacheNames = "payment-events", key = "#event.getPayment().getPaymentId().toString()")
+ private PaymentEvent addEvent(PaymentEvent event) {
+ event = eventRepository.saveAndFlush(event);
+ return event;
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/event/PaymentEvent.java b/payment/payment-web/src/main/java/demo/event/PaymentEvent.java
new file mode 100644
index 0000000..9f2e37b
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/PaymentEvent.java
@@ -0,0 +1,73 @@
+package demo.event;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import demo.payment.Payment;
+import demo.domain.BaseEntity;
+
+import javax.persistence.*;
+
+/**
+ * The domain event {@link PaymentEvent} tracks the type and state of events as
+ * applied to the {@link Payment} domain object. This event resource can be used
+ * to event source the aggregate state of {@link Payment}.
+ *
+ * This event resource also provides a transaction log that can be used to append
+ * actions to the event.
+ *
+ * @author kbastani
+ */
+@Entity
+public class PaymentEvent extends BaseEntity {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ private PaymentEventType type;
+
+ @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JsonIgnore
+ private Payment payment;
+
+ public PaymentEvent() {
+ }
+
+ public PaymentEvent(PaymentEventType type) {
+ this.type = type;
+ }
+
+ @JsonIgnore
+ public Long getEventId() {
+ return id;
+ }
+
+ public void setEventId(Long id) {
+ this.id = id;
+ }
+
+ public PaymentEventType getType() {
+ return type;
+ }
+
+ public void setType(PaymentEventType type) {
+ this.type = type;
+ }
+
+ public Payment getPayment() {
+ return payment;
+ }
+
+ public void setPayment(Payment payment) {
+ this.payment = payment;
+ }
+
+ @Override
+ public String toString() {
+ return "PaymentEvent{" +
+ "id=" + id +
+ ", type=" + type +
+ ", payment=" + payment +
+ "} " + super.toString();
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/event/PaymentEventType.java b/payment/payment-web/src/main/java/demo/event/PaymentEventType.java
new file mode 100644
index 0000000..483ea22
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/PaymentEventType.java
@@ -0,0 +1,18 @@
+package demo.event;
+
+import demo.payment.Payment;
+import demo.payment.PaymentStatus;
+
+/**
+ * The {@link PaymentEventType} represents a collection of possible events that describe
+ * state transitions of {@link PaymentStatus} on the {@link Payment} aggregate.
+ *
+ * @author kbastani
+ */
+public enum PaymentEventType {
+ PAYMENT_CREATED,
+ PAYMENT_PENDING,
+ PAYMENT_PROCESSED,
+ PAYMENT_FAILED,
+ PAYMENT_SUCCEEDED
+}
diff --git a/payment/payment-web/src/main/java/demo/event/PaymentEvents.java b/payment/payment-web/src/main/java/demo/event/PaymentEvents.java
new file mode 100644
index 0000000..ff0d4cf
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/event/PaymentEvents.java
@@ -0,0 +1,74 @@
+package demo.event;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import demo.payment.Payment;
+import demo.payment.PaymentController;
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.LinkBuilder;
+import org.springframework.hateoas.Resources;
+
+import java.io.Serializable;
+import java.util.List;
+
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
+
+/**
+ * The {@link PaymentEvents} is a hypermedia collection of {@link PaymentEvent} resources.
+ *
+ * @author kbastani
+ */
+public class PaymentEvents extends Resources implements Serializable {
+
+ private Long paymentId;
+
+ /**
+ * Create a new {@link PaymentEvents} hypermedia resources collection for an {@link Payment}.
+ *
+ * @param paymentId is the unique identifier for the {@link Payment}
+ * @param content is the collection of {@link PaymentEvents} attached to the {@link Payment}
+ */
+ public PaymentEvents(Long paymentId, List content) {
+ this(content);
+ this.paymentId = paymentId;
+
+ // Add hypermedia links to resources parent
+ add(linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(paymentId)
+ .slash("events")
+ .withSelfRel(),
+ linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(paymentId)
+ .withRel("payment"));
+
+ LinkBuilder linkBuilder = linkTo(EventController.class);
+
+ // Add hypermedia links to each item of the collection
+ content.stream().parallel().forEach(event -> event.add(
+ linkBuilder.slash("events")
+ .slash(event.getEventId())
+ .withSelfRel()
+ ));
+ }
+
+ /**
+ * Creates a {@link Resources} instance with the given content and {@link Link}s (optional).
+ *
+ * @param content must not be {@literal null}.
+ * @param links the links to be added to the {@link Resources}.
+ */
+ private PaymentEvents(Iterable content, Link... links) {
+ super(content, links);
+ }
+
+ /**
+ * Get the {@link Payment} identifier that the {@link PaymentEvents} apply to.
+ *
+ * @return the payment identifier
+ */
+ @JsonIgnore
+ public Long getPaymentId() {
+ return paymentId;
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/Payment.java b/payment/payment-web/src/main/java/demo/payment/Payment.java
new file mode 100644
index 0000000..146563a
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/Payment.java
@@ -0,0 +1,106 @@
+package demo.payment;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import demo.domain.BaseEntity;
+import demo.event.PaymentEvent;
+
+import javax.persistence.*;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The {@link Payment} domain object contains information related to
+ * a user's payment. The status of an payment is event sourced using
+ * events logged to the {@link PaymentEvent} collection attached to
+ * this resource.
+ *
+ * @author kbastani
+ */
+@Entity
+public class Payment extends BaseEntity {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ private Set events = new HashSet<>();
+
+ @Enumerated(value = EnumType.STRING)
+ private PaymentStatus status;
+
+ private Double amount;
+ private Long orderId;
+
+ @Enumerated(value = EnumType.STRING)
+ private PaymentMethod paymentMethod;
+
+ public Payment() {
+ status = PaymentStatus.PAYMENT_CREATED;
+ }
+
+ public Payment(Double amount, PaymentMethod paymentMethod) {
+ this.amount = amount;
+ this.paymentMethod = paymentMethod;
+ }
+
+ @JsonIgnore
+ public Long getPaymentId() {
+ return id;
+ }
+
+ public void setPaymentId(Long id) {
+ this.id = id;
+ }
+
+ @JsonIgnore
+ public Set getEvents() {
+ return events;
+ }
+
+ public void setEvents(Set events) {
+ this.events = events;
+ }
+
+ public PaymentStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(PaymentStatus status) {
+ this.status = status;
+ }
+
+ public Double getAmount() {
+ return amount;
+ }
+
+ public void setAmount(Double amount) {
+ this.amount = amount;
+ }
+
+ public PaymentMethod getPaymentMethod() {
+ return paymentMethod;
+ }
+
+ public void setPaymentMethod(PaymentMethod paymentMethod) {
+ this.paymentMethod = paymentMethod;
+ }
+
+ @JsonIgnore
+ public Long getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(Long orderId) {
+ this.orderId = orderId;
+ }
+
+ @Override
+ public String toString() {
+ return "Payment{" +
+ "id=" + id +
+ ", events=" + events +
+ ", status=" + status +
+ "} " + super.toString();
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentCommand.java b/payment/payment-web/src/main/java/demo/payment/PaymentCommand.java
new file mode 100644
index 0000000..f087cff
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentCommand.java
@@ -0,0 +1,13 @@
+package demo.payment;
+
+/**
+ * The {@link PaymentCommand} represents an action that can be performed to an
+ * {@link Payment} aggregate. Commands initiate an action that can mutate the state of
+ * an payment entity as it transitions between {@link PaymentStatus} values.
+ *
+ * @author kbastani
+ */
+public enum PaymentCommand {
+ CONNECT_ORDER,
+ PROCESS_PAYMENT
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentCommands.java b/payment/payment-web/src/main/java/demo/payment/PaymentCommands.java
new file mode 100644
index 0000000..882d62f
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentCommands.java
@@ -0,0 +1,12 @@
+package demo.payment;
+
+import org.springframework.hateoas.ResourceSupport;
+
+/**
+ * A hypermedia resource that describes the collection of commands that
+ * can be applied to a {@link Payment} aggregate.
+ *
+ * @author kbastani
+ */
+public class PaymentCommands extends ResourceSupport {
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentController.java b/payment/payment-web/src/main/java/demo/payment/PaymentController.java
new file mode 100644
index 0000000..e74df77
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentController.java
@@ -0,0 +1,248 @@
+package demo.payment;
+
+import demo.event.PaymentEvent;
+import demo.event.PaymentEvents;
+import demo.event.EventController;
+import demo.event.EventService;
+import org.springframework.hateoas.LinkBuilder;
+import org.springframework.hateoas.Resource;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Optional;
+
+import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
+
+@RestController
+@RequestMapping("/v1")
+public class PaymentController {
+
+ private final PaymentService paymentService;
+ private final EventService eventService;
+
+ public PaymentController(PaymentService paymentService, EventService eventService) {
+ this.paymentService = paymentService;
+ this.eventService = eventService;
+ }
+
+ @PostMapping(path = "/payments")
+ public ResponseEntity createPayment(@RequestBody Payment payment) {
+ return Optional.ofNullable(createPaymentResource(payment))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
+ .orElseThrow(() -> new RuntimeException("Payment creation failed"));
+ }
+
+ @PutMapping(path = "/payments/{id}")
+ public ResponseEntity updatePayment(@RequestBody Payment payment, @PathVariable Long id) {
+ return Optional.ofNullable(updatePaymentResource(id, payment))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("Payment update failed"));
+ }
+
+ @GetMapping(path = "/payments/{id}")
+ public ResponseEntity getPayment(@PathVariable Long id) {
+ return Optional.ofNullable(getPaymentResource(id))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
+ }
+
+ @DeleteMapping(path = "/payments/{id}")
+ public ResponseEntity deletePayment(@PathVariable Long id) {
+ return Optional.ofNullable(paymentService.deletePayment(id))
+ .map(e -> new ResponseEntity<>(HttpStatus.NO_CONTENT))
+ .orElseThrow(() -> new RuntimeException("Payment deletion failed"));
+ }
+
+ @GetMapping(path = "/payments/{id}/events")
+ public ResponseEntity getPaymentEvents(@PathVariable Long id) {
+ return Optional.of(getPaymentEventResources(id))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("Could not get payment events"));
+ }
+
+ @PostMapping(path = "/payments/{id}/events")
+ public ResponseEntity createPayment(@PathVariable Long id, @RequestBody PaymentEvent event) {
+ return Optional.ofNullable(appendEventResource(id, event))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
+ .orElseThrow(() -> new RuntimeException("Append payment event failed"));
+ }
+
+ @GetMapping(path = "/payments/{id}/commands")
+ public ResponseEntity getPaymentCommands(@PathVariable Long id) {
+ return Optional.ofNullable(getCommandsResource(id))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The payment could not be found"));
+ }
+
+ @GetMapping(path = "/payments/{id}/commands/connectOrder")
+ public ResponseEntity connectOrder(@PathVariable Long id) {
+ return Optional.ofNullable(getPaymentResource(
+ paymentService.applyCommand(id, PaymentCommand.CONNECT_ORDER)))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The command could not be applied"));
+ }
+
+ @GetMapping(path = "/payments/{id}/commands/processPayment")
+ public ResponseEntity processPayment(@PathVariable Long id) {
+ return Optional.ofNullable(getPaymentResource(
+ paymentService.applyCommand(id, PaymentCommand.PROCESS_PAYMENT)))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.OK))
+ .orElseThrow(() -> new RuntimeException("The command could not be applied"));
+ }
+
+ /**
+ * Retrieves a hypermedia resource for {@link Payment} with the specified identifier.
+ *
+ * @param id is the unique identifier for looking up the {@link Payment} entity
+ * @return a hypermedia resource for the fetched {@link Payment}
+ */
+ private Resource getPaymentResource(Long id) {
+ Resource paymentResource = null;
+
+ // Get the payment for the provided id
+ Payment payment = paymentService.getPayment(id);
+
+ // If the payment exists, wrap the hypermedia response
+ if (payment != null)
+ paymentResource = getPaymentResource(payment);
+
+
+ return paymentResource;
+ }
+
+ /**
+ * Creates a new {@link Payment} entity and persists the result to the repository.
+ *
+ * @param payment is the {@link Payment} model used to create a new payment
+ * @return a hypermedia resource for the newly created {@link Payment}
+ */
+ private Resource createPaymentResource(Payment payment) {
+ Assert.notNull(payment, "Payment body must not be null");
+
+ // Create the new payment
+ payment = paymentService.registerPayment(payment);
+
+ return getPaymentResource(payment);
+ }
+
+ /**
+ * Update a {@link Payment} entity for the provided identifier.
+ *
+ * @param id is the unique identifier for the {@link Payment} update
+ * @param payment is the entity representation containing any updated {@link Payment} fields
+ * @return a hypermedia resource for the updated {@link Payment}
+ */
+ private Resource updatePaymentResource(Long id, Payment payment) {
+ return getPaymentResource(paymentService.updatePayment(id, payment));
+ }
+
+ /**
+ * Appends an {@link PaymentEvent} domain event to the event log of the {@link Payment}
+ * aggregate with the specified paymentId.
+ *
+ * @param paymentId is the unique identifier for the {@link Payment}
+ * @param event is the {@link PaymentEvent} that attempts to alter the state of the {@link Payment}
+ * @return a hypermedia resource for the newly appended {@link PaymentEvent}
+ */
+ private Resource appendEventResource(Long paymentId, PaymentEvent event) {
+ Resource eventResource = null;
+
+ event = paymentService.appendEvent(paymentId, event);
+
+ if (event != null) {
+ eventResource = new Resource<>(event,
+ linkTo(EventController.class)
+ .slash("events")
+ .slash(event.getEventId())
+ .withSelfRel(),
+ linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(paymentId)
+ .withRel("payment")
+ );
+ }
+
+ return eventResource;
+ }
+
+ /**
+ * Get the {@link PaymentCommand} hypermedia resource that lists the available commands that can be applied
+ * to an {@link Payment} entity.
+ *
+ * @param id is the {@link Payment} identifier to provide command links for
+ * @return an {@link PaymentCommands} with a collection of embedded command links
+ */
+ private PaymentCommands getCommandsResource(Long id) {
+ // Get the payment resource for the identifier
+ Resource paymentResource = getPaymentResource(id);
+
+ // Create a new payment commands hypermedia resource
+ PaymentCommands commandResource = new PaymentCommands();
+
+ // Add payment command hypermedia links
+ if (paymentResource != null) {
+ commandResource.add(
+ getCommandLinkBuilder(id)
+ .slash("connectOrder")
+ .withRel("connectOrder"),
+ getCommandLinkBuilder(id)
+ .slash("processPayment")
+ .withRel("processPayment")
+ );
+ }
+
+ return commandResource;
+ }
+
+ /**
+ * Get {@link PaymentEvents} for the supplied {@link Payment} identifier.
+ *
+ * @param id is the unique identifier of the {@link Payment}
+ * @return a list of {@link PaymentEvent} wrapped in a hypermedia {@link PaymentEvents} resource
+ */
+ private PaymentEvents getPaymentEventResources(Long id) {
+ return new PaymentEvents(id, eventService.getPaymentEvents(id));
+ }
+
+ /**
+ * Generate a {@link LinkBuilder} for generating the {@link PaymentCommands}.
+ *
+ * @param id is the unique identifier for a {@link Payment}
+ * @return a {@link LinkBuilder} for the {@link PaymentCommands}
+ */
+ private LinkBuilder getCommandLinkBuilder(Long id) {
+ return linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(id)
+ .slash("commands");
+ }
+
+ /**
+ * Get a hypermedia enriched {@link Payment} entity.
+ *
+ * @param payment is the {@link Payment} to enrich with hypermedia links
+ * @return is a hypermedia enriched resource for the supplied {@link Payment} entity
+ */
+ private Resource getPaymentResource(Payment payment) {
+ Resource paymentResource;
+
+ // Prepare hypermedia response
+ paymentResource = new Resource<>(payment,
+ linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(payment.getPaymentId())
+ .withSelfRel(),
+ linkTo(PaymentController.class)
+ .slash("payments")
+ .slash(payment.getPaymentId())
+ .slash("events")
+ .withRel("events"),
+ getCommandLinkBuilder(payment.getPaymentId())
+ .withRel("commands")
+ );
+
+ return paymentResource;
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentMethod.java b/payment/payment-web/src/main/java/demo/payment/PaymentMethod.java
new file mode 100644
index 0000000..1591721
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentMethod.java
@@ -0,0 +1,5 @@
+package demo.payment;
+
+public enum PaymentMethod {
+ CREDIT_CARD
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java b/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java
new file mode 100644
index 0000000..5975dc4
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentRepository.java
@@ -0,0 +1,9 @@
+package demo.payment;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.repository.query.Param;
+
+public interface PaymentRepository extends JpaRepository {
+
+ Payment findPaymentByOrderId(@Param("orderId") Long orderId);
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentService.java b/payment/payment-web/src/main/java/demo/payment/PaymentService.java
new file mode 100644
index 0000000..4b1a88b
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentService.java
@@ -0,0 +1,174 @@
+package demo.payment;
+
+import demo.event.ConsistencyModel;
+import demo.event.EventService;
+import demo.event.PaymentEvent;
+import demo.event.PaymentEventType;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.CacheConfig;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+import java.util.Objects;
+
+/**
+ * The {@link PaymentService} provides transactional support for managing {@link Payment}
+ * entities. This service also provides event sourcing support for {@link PaymentEvent}.
+ * Events can be appended to an {@link Payment}, which contains a append-only log of
+ * actions that can be used to support remediation for distributed transactions that encountered
+ * a partial failure.
+ *
+ * @author kbastani
+ */
+@Service
+@CacheConfig(cacheNames = {"payments"})
+public class PaymentService {
+
+ private final PaymentRepository paymentRepository;
+ private final EventService eventService;
+ private final CacheManager cacheManager;
+
+ public PaymentService(PaymentRepository paymentRepository, EventService eventService, CacheManager cacheManager) {
+ this.paymentRepository = paymentRepository;
+ this.eventService = eventService;
+ this.cacheManager = cacheManager;
+ }
+
+ @CacheEvict(cacheNames = "payments", key = "#payment.getPaymentId().toString()")
+ public Payment registerPayment(Payment payment) {
+
+ payment = createPayment(payment);
+
+ cacheManager.getCache("payments")
+ .evict(payment.getPaymentId());
+
+ // Trigger the payment creation event
+ PaymentEvent event = appendEvent(payment.getPaymentId(),
+ new PaymentEvent(PaymentEventType.PAYMENT_CREATED));
+
+ // Attach payment identifier
+ event.getPayment().setPaymentId(payment.getPaymentId());
+
+ // Return the result
+ return event.getPayment();
+ }
+
+ /**
+ * Create a new {@link Payment} entity.
+ *
+ * @param payment is the {@link Payment} to create
+ * @return the newly created {@link Payment}
+ */
+ @CacheEvict(cacheNames = "payments", key = "#payment.getPaymentId().toString()")
+ public Payment createPayment(Payment payment) {
+
+ // Save the payment to the repository
+ payment = paymentRepository.saveAndFlush(payment);
+
+ return payment;
+ }
+
+ /**
+ * Get an {@link Payment} entity for the supplied identifier.
+ *
+ * @param id is the unique identifier of a {@link Payment} entity
+ * @return an {@link Payment} entity
+ */
+ @Cacheable(cacheNames = "payments", key = "#id.toString()")
+ public Payment getPayment(Long id) {
+ return paymentRepository.findOne(id);
+ }
+
+ /**
+ * Update an {@link Payment} entity with the supplied identifier.
+ *
+ * @param id is the unique identifier of the {@link Payment} entity
+ * @param payment is the {@link Payment} containing updated fields
+ * @return the updated {@link Payment} entity
+ */
+ @CachePut(cacheNames = "payments", key = "#id.toString()")
+ public Payment updatePayment(Long id, Payment payment) {
+ Assert.notNull(id, "Payment id must be present in the resource URL");
+ Assert.notNull(payment, "Payment request body cannot be null");
+
+ if (payment.getPaymentId() != null) {
+ Assert.isTrue(Objects.equals(id, payment.getPaymentId()),
+ "The payment id in the request body must match the resource URL");
+ } else {
+ payment.setPaymentId(id);
+ }
+
+ Assert.state(paymentRepository.exists(id),
+ "The payment with the supplied id does not exist");
+
+ Payment currentPayment = paymentRepository.findOne(id);
+ currentPayment.setStatus(payment.getStatus());
+
+ return paymentRepository.save(currentPayment);
+ }
+
+ /**
+ * Delete the {@link Payment} with the supplied identifier.
+ *
+ * @param id is the unique identifier for the {@link Payment}
+ */
+ @CacheEvict(cacheNames = "payments", key = "#id.toString()")
+ public Boolean deletePayment(Long id) {
+ Assert.state(paymentRepository.exists(id),
+ "The payment with the supplied id does not exist");
+ this.paymentRepository.delete(id);
+ return true;
+ }
+
+ /**
+ * Append a new {@link PaymentEvent} to the {@link Payment} reference for the supplied identifier.
+ *
+ * @param paymentId is the unique identifier for the {@link Payment}
+ * @param event is the {@link PaymentEvent} to append to the {@link Payment} entity
+ * @return the newly appended {@link PaymentEvent}
+ */
+ public PaymentEvent appendEvent(Long paymentId, PaymentEvent event) {
+ return appendEvent(paymentId, event, ConsistencyModel.ACID);
+ }
+
+ /**
+ * Append a new {@link PaymentEvent} to the {@link Payment} reference for the supplied identifier.
+ *
+ * @param paymentId is the unique identifier for the {@link Payment}
+ * @param event is the {@link PaymentEvent} to append to the {@link Payment} entity
+ * @return the newly appended {@link PaymentEvent}
+ */
+ public PaymentEvent appendEvent(Long paymentId, PaymentEvent event, ConsistencyModel consistencyModel) {
+ Payment payment = getPayment(paymentId);
+ Assert.notNull(payment, "The payment with the supplied id does not exist");
+ event.setPayment(payment);
+ event = eventService.createEvent(paymentId, event);
+ payment.getEvents().add(event);
+ paymentRepository.saveAndFlush(payment);
+ eventService.raiseEvent(event, consistencyModel);
+ return event;
+ }
+
+ /**
+ * Apply an {@link PaymentCommand} to the {@link Payment} with a specified identifier.
+ *
+ * @param id is the unique identifier of the {@link Payment}
+ * @param paymentCommand is the command to apply to the {@link Payment}
+ * @return a hypermedia resource containing the updated {@link Payment}
+ */
+ @CachePut(cacheNames = "payments", key = "#id.toString()")
+ public Payment applyCommand(Long id, PaymentCommand paymentCommand) {
+ Payment payment = getPayment(id);
+
+ Assert.notNull(payment, "The payment for the supplied id could not be found");
+
+ PaymentStatus status = payment.getStatus();
+
+ // TODO: Implement
+
+ return payment;
+ }
+}
diff --git a/payment/payment-web/src/main/java/demo/payment/PaymentStatus.java b/payment/payment-web/src/main/java/demo/payment/PaymentStatus.java
new file mode 100644
index 0000000..eca6bbd
--- /dev/null
+++ b/payment/payment-web/src/main/java/demo/payment/PaymentStatus.java
@@ -0,0 +1,16 @@
+package demo.payment;
+
+/**
+ * The {@link PaymentStatus} describes the state of an {@link Payment}.
+ * The aggregate state of a {@link Payment} is sourced from attached domain
+ * events in the form of {@link demo.event.PaymentEvent}.
+ *
+ * @author kbastani
+ */
+public enum PaymentStatus {
+ PAYMENT_CREATED,
+ PAYMENT_PENDING,
+ PAYMENT_PROCESSED,
+ PAYMENT_FAILED,
+ PAYMENT_SUCCEEDED
+}
diff --git a/payment/payment-web/src/main/resources/application.yml b/payment/payment-web/src/main/resources/application.yml
new file mode 100644
index 0000000..42ae822
--- /dev/null
+++ b/payment/payment-web/src/main/resources/application.yml
@@ -0,0 +1,17 @@
+spring:
+ profiles:
+ active: development
+---
+spring:
+ profiles: development
+ cloud:
+ stream:
+ bindings:
+ output:
+ destination: payment
+ contentType: 'application/json'
+ redis:
+ host: localhost
+ port: 6379
+server:
+ port: 8080
\ No newline at end of file
diff --git a/payment/payment-web/src/main/resources/bootstrap.yml b/payment/payment-web/src/main/resources/bootstrap.yml
new file mode 100644
index 0000000..e2c706f
--- /dev/null
+++ b/payment/payment-web/src/main/resources/bootstrap.yml
@@ -0,0 +1,4 @@
+spring:
+ application:
+ name: payment-web
+---
\ No newline at end of file
diff --git a/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java
new file mode 100644
index 0000000..82d4668
--- /dev/null
+++ b/payment/payment-web/src/test/java/demo/payment/PaymentControllerTest.java
@@ -0,0 +1,43 @@
+package demo.payment;
+
+import demo.event.EventService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.BDDMockito.given;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringRunner.class)
+@WebMvcTest(PaymentController.class)
+public class PaymentControllerTest {
+
+ @Autowired
+ private MockMvc mvc;
+
+ @MockBean
+ private PaymentService paymentService;
+
+ @MockBean
+ private EventService eventService;
+
+ @Test
+ public void getUserPaymentResourceShouldReturnPayment() throws Exception {
+ String content = "{\"paymentMethod\": \"CREDIT_CARD\", \"amount\": 42.0 }";
+
+ Payment payment = new Payment(42.0, PaymentMethod.CREDIT_CARD);
+
+ given(this.paymentService.getPayment(1L))
+ .willReturn(payment);
+
+ this.mvc.perform(get("/v1/payments/1").accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk()).andExpect(content().json(content));
+ }
+}
diff --git a/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java
new file mode 100644
index 0000000..e14a6e3
--- /dev/null
+++ b/payment/payment-web/src/test/java/demo/payment/PaymentServiceTests.java
@@ -0,0 +1,45 @@
+package demo.payment;
+
+import demo.event.EventService;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.cache.CacheManager;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
+@RunWith(SpringRunner.class)
+public class PaymentServiceTests {
+
+ @MockBean
+ private EventService eventService;
+
+ @MockBean
+ private PaymentRepository paymentRepository;
+
+ @MockBean
+ private CacheManager cacheManager;
+
+ private PaymentService paymentService;
+
+ @Before
+ public void before() {
+ paymentService = new PaymentService(paymentRepository, eventService, cacheManager);
+ }
+
+ @Test
+ public void getPaymentReturnsPayment() throws Exception {
+ Payment expected = new Payment(42.0, PaymentMethod.CREDIT_CARD);
+
+ given(this.paymentRepository.findOne(1L)).willReturn(expected);
+
+ Payment actual = paymentService.getPayment(1L);
+
+ assertThat(actual).isNotNull();
+ assertThat(actual.getPaymentMethod()).isEqualTo(PaymentMethod.CREDIT_CARD);
+ assertThat(actual.getAmount()).isEqualTo(42.0);
+ }
+}
\ No newline at end of file
diff --git a/payment/payment-web/src/test/resources/data-h2.sql b/payment/payment-web/src/test/resources/data-h2.sql
new file mode 100644
index 0000000..aae6a4c
--- /dev/null
+++ b/payment/payment-web/src/test/resources/data-h2.sql
@@ -0,0 +1 @@
+INSERT INTO PAYMENT(ID, FIRST_NAME, LAST_NAME, EMAIL) values (1, 'John', 'Doe', 'john.doe@example.com');
diff --git a/payment/payment-worker/pom.xml b/payment/payment-worker/pom.xml
new file mode 100644
index 0000000..01b16a2
--- /dev/null
+++ b/payment/payment-worker/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+
+ payment-worker
+ 0.0.1-SNAPSHOT
+ jar
+
+ payment-worker
+
+
+ org.kbastani
+ payment
+ 1.0-SNAPSHOT
+ ../
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-hateoas
+
+
+ org.springframework.cloud
+ spring-cloud-starter-stream-rabbit
+
+
+ org.springframework.boot
+ spring-boot-starter-integration
+
+
+ org.springframework.statemachine
+ spring-statemachine-core
+ ${spring-statemachine-core.version}
+
+
+ org.kbastani
+ spring-boot-starter-aws-lambda
+ ${spring-boot-starter-aws-lambda.version}
+
+
+ com.amazonaws
+ aws-java-sdk-sts
+ ${aws-java-sdk-sts.version}
+
+
+ com.amazonaws
+ aws-java-sdk-lambda
+
+
+
+ com.jayway.jsonpath
+ json-path
+ ${json-path.version}
+
+
+
+
+
+
+ com.amazonaws
+ aws-java-sdk-bom
+ ${aws-java-sdk-sts.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
diff --git a/payment/payment-worker/src/main/java/demo/PaymentStreamModuleApplication.java b/payment/payment-worker/src/main/java/demo/PaymentStreamModuleApplication.java
new file mode 100644
index 0000000..aa5db35
--- /dev/null
+++ b/payment/payment-worker/src/main/java/demo/PaymentStreamModuleApplication.java
@@ -0,0 +1,16 @@
+package demo;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.hateoas.config.EnableHypermediaSupport;
+import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
+
+@SpringBootApplication
+@EnableHypermediaSupport(type = {HypermediaType.HAL})
+public class PaymentStreamModuleApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(PaymentStreamModuleApplication.class, args);
+ }
+}
+
+
diff --git a/payment/payment-worker/src/main/java/demo/config/AwsLambdaConfig.java b/payment/payment-worker/src/main/java/demo/config/AwsLambdaConfig.java
new file mode 100644
index 0000000..47022a5
--- /dev/null
+++ b/payment/payment-worker/src/main/java/demo/config/AwsLambdaConfig.java
@@ -0,0 +1,25 @@
+package demo.config;
+
+import amazon.aws.AWSLambdaConfigurerAdapter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import demo.function.LambdaFunctions;
+import demo.util.LambdaUtil;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+
+@Configuration
+@Profile("cloud")
+public class AwsLambdaConfig {
+
+ @Bean
+ public LambdaFunctions lambdaInvoker(AWSLambdaConfigurerAdapter configurerAdapter) {
+ return configurerAdapter
+ .getFunctionInstance(LambdaFunctions.class);
+ }
+
+ @Bean
+ public LambdaUtil lambdaUtil(ObjectMapper objectMapper) {
+ return new LambdaUtil(objectMapper);
+ }
+}
diff --git a/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java b/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java
new file mode 100644
index 0000000..ffab680
--- /dev/null
+++ b/payment/payment-worker/src/main/java/demo/config/StateMachineConfig.java
@@ -0,0 +1,220 @@
+package demo.config;
+
+import demo.payment.Payment;
+import demo.payment.PaymentStatus;
+import demo.event.PaymentEvent;
+import demo.event.PaymentEventType;
+import demo.function.*;
+import org.apache.log4j.Logger;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.hateoas.client.Traverson;
+import org.springframework.statemachine.StateContext;
+import org.springframework.statemachine.StateMachine;
+import org.springframework.statemachine.action.Action;
+import org.springframework.statemachine.config.EnableStateMachineFactory;
+import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
+import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
+import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
+
+import java.net.URI;
+import java.util.EnumSet;
+
+/**
+ * A configuration adapter for describing a {@link StateMachine} factory that maps actions to functional
+ * expressions. Actions are executed during transitions between a source state and a target state.
+ *
+ * A state machine provides a robust declarative language for describing the state of an {@link Payment}
+ * resource given a sequence of ordered {@link demo.event.PaymentEvents}. When an event is received
+ * in {@link demo.event.PaymentEventStream}, an in-memory state machine is fully replicated given the
+ * {@link demo.event.PaymentEvents} attached to an {@link Payment} resource.
+ *
+ * @author kbastani
+ */
+@Configuration
+@EnableStateMachineFactory
+public class StateMachineConfig extends EnumStateMachineConfigurerAdapter {
+
+ final private Logger log = Logger.getLogger(StateMachineConfig.class);
+
+ /**
+ * Configures the initial conditions of a new in-memory {@link StateMachine} for {@link Payment}.
+ *
+ * @param states is the {@link StateMachineStateConfigurer} used to describe the initial condition
+ */
+ @Override
+ public void configure(StateMachineStateConfigurer states) {
+ try {
+ // Describe the initial condition of the payment state machine
+ states.withStates()
+ .initial(PaymentStatus.PAYMENT_CREATED)
+ .states(EnumSet.allOf(PaymentStatus.class));
+ } catch (Exception e) {
+ throw new RuntimeException("State machine configuration failed", e);
+ }
+ }
+
+ /**
+ * Configures the {@link StateMachine} that describes how {@link PaymentEventType} drives the state
+ * of an {@link Payment}. Events are applied as transitions from a source {@link PaymentStatus} to
+ * a target {@link PaymentStatus}. An {@link Action} is attached to each transition, which maps to a
+ * function that is executed in the context of an {@link PaymentEvent}.
+ *
+ * @param transitions is the {@link StateMachineTransitionConfigurer} used to describe state transitions
+ */
+ @Override
+ public void configure(StateMachineTransitionConfigurer transitions) {
+ try {
+ // Describe state machine transitions for payments
+ transitions.withExternal()
+ .source(PaymentStatus.PAYMENT_CREATED)
+ .target(PaymentStatus.PAYMENT_CREATED)
+ .event(PaymentEventType.PAYMENT_CREATED)
+ .action(paymentCreated())
+ .and()
+ .withExternal()
+ .source(PaymentStatus.PAYMENT_CREATED)
+ .target(PaymentStatus.PAYMENT_PENDING)
+ .event(PaymentEventType.PAYMENT_PENDING)
+ .action(paymentPending())
+ .and()
+ .withExternal()
+ .source(PaymentStatus.PAYMENT_PENDING)
+ .target(PaymentStatus.PAYMENT_PROCESSED)
+ .event(PaymentEventType.PAYMENT_PROCESSED)
+ .action(paymentProcessed())
+ .and()
+ .withExternal()
+ .source(PaymentStatus.PAYMENT_PROCESSED)
+ .target(PaymentStatus.PAYMENT_SUCCEEDED)
+ .event(PaymentEventType.PAYMENT_SUCCEEDED)
+ .action(paymentSucceeded())
+ .and()
+ .withExternal()
+ .source(PaymentStatus.PAYMENT_PROCESSED)
+ .target(PaymentStatus.PAYMENT_FAILED)
+ .event(PaymentEventType.PAYMENT_FAILED)
+ .action(paymentFailed());
+ } catch (Exception e) {
+ throw new RuntimeException("Could not configure state machine transitions", e);
+ }
+ }
+
+ /**
+ * Functions are mapped to actions that are triggered during the replication of a state machine. Functions
+ * should only be executed after the state machine has completed replication. This method checks the state
+ * context of the machine for an {@link PaymentEvent}, which signals that the state machine is finished
+ * replication.
+ *
+ * The {@link PaymentFunction} argument is only applied if an {@link PaymentEvent} is provided as a
+ * message header in the {@link StateContext}.
+ *
+ * @param context is the state machine context that may include an {@link PaymentEvent}
+ * @param paymentFunction is the payment function to apply after the state machine has completed replication
+ * @return an {@link PaymentEvent} only if this event has not yet been processed, otherwise returns null
+ */
+ private PaymentEvent applyEvent(StateContext context, PaymentFunction paymentFunction) {
+ PaymentEvent paymentEvent = null;
+
+ // Log out the progress of the state machine replication
+ log.info("Replicate event: " + context.getMessage().getPayload());
+
+ // The machine is finished replicating when an PaymentEvent is found in the message header
+ if (context.getMessageHeader("event") != null) {
+ paymentEvent = (PaymentEvent) context.getMessageHeader("event");
+ log.info("State machine replicated: " + paymentEvent.getType());
+
+ // Apply the provided function to the PaymentEvent
+ paymentFunction.apply(paymentEvent);
+ }
+
+ return paymentEvent;
+ }
+
+ @Bean
+ public Action paymentCreated() {
+ return context -> applyEvent(context,
+ new PaymentCreated(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("payment").getHref());
+ // Get the payment resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("payment").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Payment.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentPending() {
+ return context -> applyEvent(context,
+ new PaymentPending(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("payment").getHref());
+ // Get the payment resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("payment").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Payment.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentProcessed() {
+ return context -> applyEvent(context,
+ new PaymentProcessed(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("payment").getHref());
+ // Get the payment resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("payment").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Payment.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentSucceeded() {
+ return context -> applyEvent(context,
+ new PaymentSucceeded(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("payment").getHref());
+ // Get the payment resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("payment").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Payment.class)
+ .getBody();
+ }));
+ }
+
+ @Bean
+ public Action paymentFailed() {
+ return context -> applyEvent(context,
+ new PaymentFailed(context, event -> {
+ log.info(event.getType() + ": " + event.getLink("payment").getHref());
+ // Get the payment resource for the event
+ Traverson traverson = new Traverson(
+ URI.create(event.getLink("payment").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ return traverson.follow("self")
+ .toEntity(Payment.class)
+ .getBody();
+ }));
+ }
+}
+
diff --git a/payment/payment-worker/src/main/java/demo/domain/BaseEntity.java b/payment/payment-worker/src/main/java/demo/domain/BaseEntity.java
new file mode 100644
index 0000000..586dbb4
--- /dev/null
+++ b/payment/payment-worker/src/main/java/demo/domain/BaseEntity.java
@@ -0,0 +1,36 @@
+package demo.domain;
+
+import org.springframework.hateoas.ResourceSupport;
+
+public class BaseEntity extends ResourceSupport {
+
+ private Long createdAt;
+ private Long lastModified;
+
+ public BaseEntity() {
+ }
+
+ public Long getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Long createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public Long getLastModified() {
+ return lastModified;
+ }
+
+ public void setLastModified(Long lastModified) {
+ this.lastModified = lastModified;
+ }
+
+ @Override
+ public String toString() {
+ return "BaseEntity{" +
+ "createdAt=" + createdAt +
+ ", lastModified=" + lastModified +
+ "} " + super.toString();
+ }
+}
diff --git a/payment/payment-worker/src/main/java/demo/event/EventController.java b/payment/payment-worker/src/main/java/demo/event/EventController.java
new file mode 100644
index 0000000..115bede
--- /dev/null
+++ b/payment/payment-worker/src/main/java/demo/event/EventController.java
@@ -0,0 +1,28 @@
+package demo.event;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Optional;
+
+@RestController
+@RequestMapping("/v1")
+public class EventController {
+
+ private EventService eventService;
+
+ public EventController(EventService eventService) {
+ this.eventService = eventService;
+ }
+
+ @PostMapping(path = "/events")
+ public ResponseEntity handleEvent(@RequestBody PaymentEvent event) {
+ return Optional.ofNullable(eventService.apply(event))
+ .map(e -> new ResponseEntity<>(e, HttpStatus.CREATED))
+ .orElseThrow(() -> new RuntimeException("Apply event failed"));
+ }
+}
diff --git a/payment/payment-worker/src/main/java/demo/event/EventService.java b/payment/payment-worker/src/main/java/demo/event/EventService.java
new file mode 100644
index 0000000..98a6286
--- /dev/null
+++ b/payment/payment-worker/src/main/java/demo/event/EventService.java
@@ -0,0 +1,82 @@
+package demo.event;
+
+import demo.payment.Payment;
+import demo.payment.PaymentStatus;
+import demo.state.StateMachineService;
+import org.apache.log4j.Logger;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.hateoas.client.Traverson;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.statemachine.StateMachine;
+import org.springframework.stereotype.Service;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class EventService {
+
+ final private Logger log = Logger.getLogger(EventService.class);
+ final private StateMachineService stateMachineService;
+
+ public EventService(StateMachineService stateMachineService) {
+ this.stateMachineService = stateMachineService;
+ }
+
+ public Payment apply(PaymentEvent paymentEvent) {
+
+ Payment result;
+
+ log.info("Payment event received: " + paymentEvent.getLink("self").getHref());
+
+ // Generate a state machine for computing the state of the payment resource
+ StateMachine stateMachine =
+ stateMachineService.getStateMachine();
+
+ // Follow the hypermedia link to fetch the attached payment
+ Traverson traverson = new Traverson(
+ URI.create(paymentEvent.getLink("payment").getHref()),
+ MediaTypes.HAL_JSON
+ );
+
+ // Get the event log for the attached payment resource
+ PaymentEvents events = traverson.follow("events")
+ .toEntity(PaymentEvents.class)
+ .getBody();
+
+ // Prepare payment event message headers
+ Map headerMap = new HashMap<>();
+ headerMap.put("event", paymentEvent);
+
+ // Replicate the current state of the payment resource
+ events.getContent()
+ .stream()
+ .sorted((a1, a2) -> a1.getCreatedAt().compareTo(a2.getCreatedAt()))
+ .forEach(e -> {
+ MessageHeaders headers = new MessageHeaders(null);
+
+ // Check to see if this is the current event
+ if (e.getLink("self").equals(paymentEvent.getLink("self"))) {
+ headers = new MessageHeaders(headerMap);
+ }
+
+ // Send the event to the state machine
+ stateMachine.sendEvent(MessageBuilder.createMessage(e.getType(), headers));
+ });
+
+
+ // Get result
+ Map