From 7ea69697925a01b9d5b76c2f556f63c49615aa41 Mon Sep 17 00:00:00 2001 From: Kenny Bastani Date: Tue, 3 Jan 2017 18:10:13 -0500 Subject: [PATCH] Finish payment workflow --- .../account/controller/AccountController.java | 48 +++++------ order/order-web/pom.xml | 5 ++ .../demo/order/action/ConnectAccount.java | 32 ++++++-- .../demo/order/action/ConnectPayment.java | 34 +++++--- .../java/demo/order/action/CreatePayment.java | 75 ++++++++--------- .../java/demo/order/action/DeleteOrder.java | 16 ++-- .../demo/order/action/ProcessPayment.java | 61 +++++++++++++- .../order/controller/OrderController.java | 18 +++-- .../main/java/demo/order/domain/Order.java | 51 ++++++++---- .../java/demo/order/domain/OrderModule.java | 10 ++- .../java/demo/payment/domain/Payment.java | 53 +++++++++++- .../demo/payment/domain/PaymentEvent.java | 81 +++++++++++++++++++ .../demo/payment/domain/PaymentEventType.java | 16 ++++ .../demo/payment/domain/PaymentModule.java | 35 ++++++++ .../demo/payment/domain/PaymentService.java | 47 +++++++++++ .../demo/payment/domain/PaymentStatus.java | 1 + .../java/demo/payment/domain/Payments.java | 24 ++++++ .../java/demo/config/StateMachineConfig.java | 47 ++++------- .../java/demo/function/PaymentSucceeded.java | 54 ++++++++++++- .../main/java/demo/order/domain/Order.java | 20 +++++ .../demo/payment/action/ConnectOrder.java | 23 +++++- .../demo/payment/action/ProcessPayment.java | 19 ++++- .../payment/controller/PaymentController.java | 4 +- .../java/demo/payment/domain/Payment.java | 8 +- .../src/main/java/demo/domain/Action.java | 13 +++ 25 files changed, 627 insertions(+), 168 deletions(-) create mode 100644 order/order-web/src/main/java/demo/payment/domain/PaymentEvent.java create mode 100644 order/order-web/src/main/java/demo/payment/domain/PaymentEventType.java create mode 100644 order/order-web/src/main/java/demo/payment/domain/PaymentModule.java create mode 100644 order/order-web/src/main/java/demo/payment/domain/PaymentService.java create mode 100644 order/order-web/src/main/java/demo/payment/domain/Payments.java diff --git a/account/account-web/src/main/java/demo/account/controller/AccountController.java b/account/account-web/src/main/java/demo/account/controller/AccountController.java index 590e33b..091615e 100644 --- a/account/account-web/src/main/java/demo/account/controller/AccountController.java +++ b/account/account-web/src/main/java/demo/account/controller/AccountController.java @@ -49,7 +49,8 @@ public class AccountController { @RequestMapping(path = "/accounts/{id}") public ResponseEntity getAccount(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource(id)) + return Optional.ofNullable(accountService.get(id)) + .map(this::getAccountResource) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); } @@ -91,57 +92,48 @@ public class AccountController { @RequestMapping(path = "/accounts/{id}/commands/confirm") public ResponseEntity confirm(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource(accountService.get(id) - .confirm())) + return Optional.ofNullable(accountService.get(id)) + .map(Account::confirm) + .map(this::getAccountResource) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @RequestMapping(path = "/accounts/{id}/commands/activate") public ResponseEntity activate(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource(accountService.get(id) - .activate())) + return Optional.ofNullable(accountService.get(id)) + .map(Account::activate) + .map(this::getAccountResource) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @RequestMapping(path = "/accounts/{id}/commands/suspend") public ResponseEntity suspend(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource(accountService.get(id) - .suspend())) + return Optional.ofNullable(accountService.get(id)) + .map(Account::suspend) + .map(this::getAccountResource) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @RequestMapping(path = "/accounts/{id}/commands/archive") public ResponseEntity archive(@PathVariable Long id) { - return Optional.ofNullable(getAccountResource(accountService.get(id) - .archive())) + return Optional.ofNullable(accountService.get(id)) + .map(Account::archive) + .map(this::getAccountResource) .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } @RequestMapping(path = "/accounts/{id}/commands/postOrder", method = RequestMethod.POST) public ResponseEntity postOrder(@PathVariable Long id, @RequestBody Order order) { - return Optional.ofNullable(accountService.get(id) - .postOrder(order)) - .map(e -> new ResponseEntity<>(e, HttpStatus.OK)) + return Optional.ofNullable(accountService.get(id)) + .map(a -> a.postOrder(order)) + .map(o -> new ResponseEntity<>(o, HttpStatus.CREATED)) .orElseThrow(() -> new RuntimeException("The command could not be applied")); } - /** - * Retrieves a hypermedia resource for {@link Account} with the specified identifier. - * - * @param id is the unique identifier for looking up the {@link Account} entity - * @return a hypermedia resource for the fetched {@link Account} - */ - private Resource getAccountResource(Long id) { - // Get the account for the provided id - Account account = accountService.get(id); - - return getAccountResource(account); - } - /** * Creates a new {@link Account} entity and persists the result to the repository. * @@ -247,17 +239,17 @@ public class AccountController { private Resource getAccountResource(Account account) { Assert.notNull(account, "Account must not be null"); - if(account.getLink("commands") == null) { + if (!account.hasLink("commands")) { // Add command link account.add(linkBuilder("getCommands", account.getIdentity()).withRel("commands")); } - if(account.getLink("events") == null) { + if (!account.hasLink("events")) { // Add get events link account.add(linkBuilder("getAccountEvents", account.getIdentity()).withRel("events")); } - if(account.getLink("orders") == null) { + if (!account.hasLink("orders")) { // Add orders link account.add(linkBuilder("getAccountOrders", account.getIdentity()).withRel("orders")); } diff --git a/order/order-web/pom.xml b/order/order-web/pom.xml index 7abf34d..cb67960 100644 --- a/order/order-web/pom.xml +++ b/order/order-web/pom.xml @@ -61,6 +61,11 @@ 1.0-SNAPSHOT + + com.jayway.jsonpath + json-path + ${json-path.version} + com.h2database h2 diff --git a/order/order-web/src/main/java/demo/order/action/ConnectAccount.java b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java index 1a712fa..5e436f8 100644 --- a/order/order-web/src/main/java/demo/order/action/ConnectAccount.java +++ b/order/order-web/src/main/java/demo/order/action/ConnectAccount.java @@ -1,15 +1,17 @@ package demo.order.action; import demo.domain.Action; -import demo.order.event.OrderEvent; -import demo.order.event.OrderEventType; import demo.order.domain.Order; import demo.order.domain.OrderModule; import demo.order.domain.OrderService; import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; +import org.apache.log4j.Logger; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; -import java.util.function.BiConsumer; +import java.util.function.BiFunction; /** * Connects an {@link Order} to an Account. @@ -18,19 +20,33 @@ import java.util.function.BiConsumer; */ @Service public class ConnectAccount extends Action { + private final Logger log = Logger.getLogger(this.getClass()); - public BiConsumer getConsumer() { + public BiFunction getFunction() { return (order, accountId) -> { - OrderService orderService = order.getModule(OrderModule.class) - .getDefaultService(); + Assert.isTrue(order.getStatus() == OrderStatus.ORDER_CREATED, "Order must be in a created state"); + + Order result; + OrderService orderService = order.getModule(OrderModule.class).getDefaultService(); // Connect the account order.setAccountId(accountId); order.setStatus(OrderStatus.ACCOUNT_CONNECTED); order = orderService.update(order); - // Trigger the account connected event - order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED, order)); + try { + // Trigger the account connected event + result = order.sendEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED, order)).getEntity(); + } catch (Exception ex) { + log.error("Could not connect order to account", ex); + order.setAccountId(null); + order.setStatus(OrderStatus.ORDER_CREATED); + orderService.update(order); + throw new IllegalStateException("Could not connect order to account", ex); + } + + return result; }; } + } diff --git a/order/order-web/src/main/java/demo/order/action/ConnectPayment.java b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java index 6c26805..9391af1 100644 --- a/order/order-web/src/main/java/demo/order/action/ConnectPayment.java +++ b/order/order-web/src/main/java/demo/order/action/ConnectPayment.java @@ -1,16 +1,18 @@ package demo.order.action; import demo.domain.Action; -import demo.order.event.OrderEvent; -import demo.order.event.OrderEventType; import demo.order.domain.Order; import demo.order.domain.OrderModule; import demo.order.domain.OrderService; import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; import demo.payment.domain.Payment; +import org.apache.log4j.Logger; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; -import java.util.function.BiConsumer; +import java.util.function.BiFunction; /** * Connects a {@link Payment} to an {@link Order}. @@ -19,19 +21,33 @@ import java.util.function.BiConsumer; */ @Service public class ConnectPayment extends Action { - public BiConsumer getConsumer() { + private final Logger log = Logger.getLogger(this.getClass()); + + public BiFunction getFunction() { return (order, paymentId) -> { + Assert.isTrue(order + .getStatus() == OrderStatus.PAYMENT_CREATED, "Order must be in a payment created state"); - OrderService orderService = order.getModule(OrderModule.class) - .getDefaultService(); + Order result; + OrderService orderService = order.getModule(OrderModule.class).getDefaultService(); - // Connect the account + // Connect the payment order.setPaymentId(paymentId); order.setStatus(OrderStatus.PAYMENT_CONNECTED); order = orderService.update(order); - // Trigger the account connected event - order.sendAsyncEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED, order)); + try { + // Trigger the payment connected event + result = order.sendEvent(new OrderEvent(OrderEventType.PAYMENT_CONNECTED, order)).getEntity(); + } catch (Exception ex) { + log.error("Could not connect payment to order", ex); + order.setPaymentId(null); + order.setStatus(OrderStatus.ORDER_CREATED); + orderService.update(order); + throw new IllegalStateException("Could not connect payment to order", ex); + } + + return result; }; } } diff --git a/order/order-web/src/main/java/demo/order/action/CreatePayment.java b/order/order-web/src/main/java/demo/order/action/CreatePayment.java index 8bdd629..f616fcd 100644 --- a/order/order-web/src/main/java/demo/order/action/CreatePayment.java +++ b/order/order-web/src/main/java/demo/order/action/CreatePayment.java @@ -1,25 +1,20 @@ package demo.order.action; import demo.domain.Action; -import demo.order.event.OrderEvent; -import demo.order.event.OrderEventType; import demo.order.domain.Order; import demo.order.domain.OrderModule; import demo.order.domain.OrderService; import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; import demo.payment.domain.Payment; import demo.payment.domain.PaymentMethod; +import demo.payment.domain.PaymentService; import org.apache.log4j.Logger; -import org.springframework.hateoas.MediaTypes; -import org.springframework.hateoas.Resource; -import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; import org.springframework.stereotype.Service; import org.springframework.util.Assert; -import org.springframework.web.client.RestTemplate; -import java.net.URI; -import java.util.function.Consumer; +import java.util.function.Function; /** * Creates a {@link Payment} for an {@link Order}. @@ -29,54 +24,54 @@ import java.util.function.Consumer; @Service public class CreatePayment extends Action { - private final Logger log = Logger.getLogger(CreatePayment.class); + private final Logger log = Logger.getLogger(this.getClass()); + private final PaymentService paymentService; - private RestTemplate restTemplate; - - public CreatePayment(RestTemplate restTemplate) { - this.restTemplate = restTemplate; + public CreatePayment(PaymentService paymentService) { + this.paymentService = paymentService; } - public Consumer getConsumer() { + public Function getFunction() { return order -> { Assert.isTrue(order.getPaymentId() == null, "Payment has already been created"); Assert.isTrue(order.getStatus() == OrderStatus.ACCOUNT_CONNECTED, "Account must be connected first"); - OrderService orderService = order.getModule(OrderModule.class) - .getDefaultService(); + // Get entity services + OrderService orderService = order.getModule(OrderModule.class).getDefaultService(); + Order result; Payment payment = new Payment(); - - // Calculate payment amount payment.setAmount(order.calculateTotal()); - - // Set payment method payment.setPaymentMethod(PaymentMethod.CREDIT_CARD); + payment = paymentService.create(payment); - // Create a new request entity - RequestEntity> requestEntity = RequestEntity.post( - URI.create("http://payment-web/v1/payments")) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaTypes.HAL_JSON) - .body(new Resource<>(payment), Resource.class); + log.info(payment); - // Update the order entity's status - Resource paymentResource = restTemplate - .exchange(requestEntity, Resource.class) - .getBody(); - - log.info(paymentResource); - - // Update the status + // Update the order status order.setStatus(OrderStatus.PAYMENT_CREATED); order = orderService.update(order); - OrderEvent event = new OrderEvent(OrderEventType.PAYMENT_CREATED, order); - event.add(paymentResource.getLink("self") - .withRel("payment")); + try { + OrderEvent event = new OrderEvent(OrderEventType.PAYMENT_CREATED, order); + event.add(payment.getLink("self").withRel("payment")); - // Trigger the payment created - order.sendAsyncEvent(event); + // Trigger payment created event + result = order.sendEvent(event).getEntity(); + } catch (Exception ex) { + log.error("The order's payment could not be created", ex); + + // Rollback the payment creation + if (payment.getIdentity() != null) + paymentService.delete(payment.getIdentity()); + + order.setPaymentId(null); + order.setStatus(OrderStatus.ACCOUNT_CONNECTED); + orderService.update(order); + + throw new IllegalStateException("Payment creation failed", ex); + } + + return result; }; } } diff --git a/order/order-web/src/main/java/demo/order/action/DeleteOrder.java b/order/order-web/src/main/java/demo/order/action/DeleteOrder.java index 254ca44..6629fc1 100644 --- a/order/order-web/src/main/java/demo/order/action/DeleteOrder.java +++ b/order/order-web/src/main/java/demo/order/action/DeleteOrder.java @@ -4,8 +4,9 @@ import demo.domain.Action; import demo.order.domain.Order; import demo.order.domain.OrderModule; import demo.payment.domain.Payment; +import demo.payment.domain.PaymentService; +import org.apache.log4j.Logger; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; import java.util.function.Consumer; @@ -17,19 +18,18 @@ import java.util.function.Consumer; @Service public class DeleteOrder extends Action { - private RestTemplate restTemplate; + private final Logger log = Logger.getLogger(this.getClass()); + private final PaymentService paymentService; - public DeleteOrder(RestTemplate restTemplate) { - this.restTemplate = restTemplate; + public DeleteOrder(PaymentService paymentService) { + this.paymentService = paymentService; } public Consumer getConsumer() { return (order) -> { // Delete payment - if (order.getPaymentId() != null) { - String href = "http://payment-web/v1/payments/" + order.getPaymentId(); - restTemplate.delete(href); - } + if (order.getPaymentId() != null) + paymentService.delete(order.getPaymentId()); // Delete order order.getModule(OrderModule.class) diff --git a/order/order-web/src/main/java/demo/order/action/ProcessPayment.java b/order/order-web/src/main/java/demo/order/action/ProcessPayment.java index a2d3431..2269e4f 100644 --- a/order/order-web/src/main/java/demo/order/action/ProcessPayment.java +++ b/order/order-web/src/main/java/demo/order/action/ProcessPayment.java @@ -2,10 +2,21 @@ package demo.order.action; import demo.domain.Action; import demo.order.domain.Order; +import demo.order.domain.OrderModule; +import demo.order.domain.OrderService; +import demo.order.domain.OrderStatus; +import demo.order.event.OrderEvent; +import demo.order.event.OrderEventType; import demo.payment.domain.Payment; +import org.apache.log4j.Logger; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.client.Traverson; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; -import java.util.function.Consumer; +import java.net.URI; +import java.util.Arrays; +import java.util.function.Function; /** * Processes a {@link Payment} for an {@link Order}. @@ -14,7 +25,51 @@ import java.util.function.Consumer; */ @Service public class ProcessPayment extends Action { - public Consumer getConsumer() { - return (order) -> {}; + + private final Logger log = Logger.getLogger(this.getClass()); + + public Function getFunction() { + return order -> { + Assert.isTrue(!Arrays + .asList(OrderStatus.PAYMENT_SUCCEEDED, OrderStatus.PAYMENT_PENDING, OrderStatus.PAYMENT_FAILED) + .contains(order.getStatus()), "Payment has already been processed"); + Assert.isTrue(order.getStatus() == OrderStatus.PAYMENT_CONNECTED, "Payment must be connected to an order"); + + Order result = order; + + // Get entity services + OrderService orderService = order.getModule(OrderModule.class).getDefaultService(); + + // Get the payment + Payment payment = order.getPayment(); + + // Update the order status + order.setStatus(OrderStatus.PAYMENT_PENDING); + order = orderService.update(order); + + try { + // Create traverson for the new order + Traverson traverson = new Traverson(URI.create(payment.getLink("self").getHref()), MediaTypes.HAL_JSON); + payment = traverson.follow("commands", "processPayment").toObject(Payment.class); + } catch (Exception ex) { + log.error("The order's payment could not be processed", ex); + + OrderEvent event = new OrderEvent(OrderEventType.PAYMENT_FAILED, order); + event.add(payment.getLink("self").withRel("payment")); + + // Trigger payment failed event + result = order.sendEvent(event).getEntity(); + } finally { + if (result.getStatus() != OrderStatus.PAYMENT_FAILED) { + OrderEvent event = new OrderEvent(OrderEventType.PAYMENT_SUCCEEDED, result); + event.add(payment.getLink("self").withRel("payment")); + + // Trigger payment created event + result = order.sendEvent(event).getEntity(); + } + } + + return result; + }; } } diff --git a/order/order-web/src/main/java/demo/order/controller/OrderController.java b/order/order-web/src/main/java/demo/order/controller/OrderController.java index 6b4c41d..cdbef36 100644 --- a/order/order-web/src/main/java/demo/order/controller/OrderController.java +++ b/order/order-web/src/main/java/demo/order/controller/OrderController.java @@ -212,23 +212,27 @@ public class OrderController { * @return is a hypermedia enriched resource for the supplied {@link Order} entity */ private Resource getOrderResource(Order order) { - if(order == null) return null; + if (order == null) return null; - // Add command link - order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands")); + if (order.getLink("commands") == null) { + // Add command link + order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands")); + } - // Add get events link - order.add(linkBuilder("getOrderEvents", order.getIdentity()).withRel("events")); + if (order.getLink("events") == null) { + // Add get events link + order.add(linkBuilder("getOrderEvents", order.getIdentity()).withRel("events")); + } // Add remote account link - if (order.getAccountId() != null) { + if (order.getAccountId() != null && order.getLink("account") == null) { Link result = getRemoteLink("account-web", "/v1/accounts/{id}", order.getAccountId(), "account"); if (result != null) order.add(result); } // Add remote payment link - if (order.getPaymentId() != null) { + if (order.getPaymentId() != null && order.getLink("payment") == null) { Link result = getRemoteLink("payment-web", "/v1/payments/{id}", order.getPaymentId(), "payment"); if (result != null) order.add(result); diff --git a/order/order-web/src/main/java/demo/order/domain/Order.java b/order/order-web/src/main/java/demo/order/domain/Order.java index 03f0b92..01f20e1 100644 --- a/order/order-web/src/main/java/demo/order/domain/Order.java +++ b/order/order-web/src/main/java/demo/order/domain/Order.java @@ -3,10 +3,13 @@ package demo.order.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import demo.domain.AbstractEntity; +import demo.domain.Aggregate; import demo.domain.Command; -import demo.order.event.OrderEvent; +import demo.domain.Module; import demo.order.action.*; import demo.order.controller.OrderController; +import demo.order.event.OrderEvent; +import demo.payment.domain.Payment; import org.springframework.hateoas.Link; import javax.persistence.*; @@ -97,34 +100,30 @@ public class Order extends AbstractEntity { @Command(method = "connectAccount", controller = OrderController.class) public Order connectAccount(Long accountId) { - getAction(ConnectAccount.class) - .getConsumer() - .accept(this, accountId); - return this; + return getAction(ConnectAccount.class) + .getFunction() + .apply(this, accountId); } @Command(method = "connectPayment", controller = OrderController.class) public Order connectPayment(Long paymentId) { - getAction(ConnectPayment.class) - .getConsumer() - .accept(this, paymentId); - return this; + return getAction(ConnectPayment.class) + .getFunction() + .apply(this, paymentId); } @Command(method = "createPayment", controller = OrderController.class) public Order createPayment() { - getAction(CreatePayment.class) - .getConsumer() - .accept(this); - return this; + return getAction(CreatePayment.class) + .getFunction() + .apply(this); } @Command(method = "processPayment", controller = OrderController.class) public Order processPayment() { - getAction(ProcessPayment.class) - .getConsumer() - .accept(this); - return this; + return getAction(ProcessPayment.class) + .getFunction() + .apply(this); } @Command(method = "reserveInventory", controller = OrderController.class) @@ -151,6 +150,24 @@ public class Order extends AbstractEntity { .sum(); } + @JsonIgnore + public Payment getPayment() { + Payment result = null; + + if (paymentId != null) + result = getModule(OrderModule.class).getPaymentService().get(paymentId); + + return result; + } + + @Override + @SuppressWarnings("unchecked") + public , A extends Aggregate> T getModule() throws + IllegalArgumentException { + OrderModule orderModule = getModule(OrderModule.class); + return (T) orderModule; + } + /** * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. */ diff --git a/order/order-web/src/main/java/demo/order/domain/OrderModule.java b/order/order-web/src/main/java/demo/order/domain/OrderModule.java index f067658..7950676 100644 --- a/order/order-web/src/main/java/demo/order/domain/OrderModule.java +++ b/order/order-web/src/main/java/demo/order/domain/OrderModule.java @@ -3,15 +3,19 @@ package demo.order.domain; import demo.domain.Module; import demo.event.EventService; import demo.order.event.OrderEvent; +import demo.payment.domain.PaymentService; @org.springframework.stereotype.Service public class OrderModule extends Module { private final OrderService orderService; + private final PaymentService paymentService; private final EventService eventService; - public OrderModule(OrderService orderService, EventService eventService) { + public OrderModule(OrderService orderService, PaymentService paymentService, EventService + eventService) { this.orderService = orderService; + this.paymentService = paymentService; this.eventService = eventService; } @@ -32,4 +36,8 @@ public class OrderModule extends Module { public EventService getDefaultEventService() { return eventService; } + + public PaymentService getPaymentService() { + return paymentService; + } } diff --git a/order/order-web/src/main/java/demo/payment/domain/Payment.java b/order/order-web/src/main/java/demo/payment/domain/Payment.java index 8951a65..7cb6129 100644 --- a/order/order-web/src/main/java/demo/payment/domain/Payment.java +++ b/order/order-web/src/main/java/demo/payment/domain/Payment.java @@ -1,7 +1,19 @@ package demo.payment.domain; -public class Payment { +import com.fasterxml.jackson.annotation.JsonProperty; +import demo.domain.Aggregate; +import demo.domain.Module; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.UriTemplate; +import java.util.ArrayList; +import java.util.List; + +public class Payment extends Aggregate { + + private Long id; + private List events = new ArrayList<>(); private Double amount; private PaymentMethod paymentMethod; private PaymentStatus status; @@ -38,10 +50,47 @@ public class Payment { this.paymentMethod = paymentMethod; } + @JsonProperty("paymentId") + @Override + public Long getIdentity() { + return id; + } + + public void setIdentity(Long id) { + this.id = id; + } + + @Override + public List getEvents() { + return events; + } + + /** + * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. + */ + @Override + public Link getId() { + return new Link(new UriTemplate("http://payment-web/v1/payments/{id}") + .with("id", TemplateVariable.VariableType + .PATH_VARIABLE) + .expand(getIdentity()) + .toString()).withSelfRel(); + } + + @Override + @SuppressWarnings("unchecked") + public , A extends Aggregate> T getModule() throws + IllegalArgumentException { + PaymentModule paymentModule = getModule(PaymentModule.class); + return (T) paymentModule; + } + @Override public String toString() { return "Payment{" + - "amount=" + amount + + "id=" + id + + ", events=" + events + + ", amount=" + amount + ", paymentMethod=" + paymentMethod + ", status=" + status + "} " + super.toString(); diff --git a/order/order-web/src/main/java/demo/payment/domain/PaymentEvent.java b/order/order-web/src/main/java/demo/payment/domain/PaymentEvent.java new file mode 100644 index 0000000..c9388e3 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/domain/PaymentEvent.java @@ -0,0 +1,81 @@ +package demo.payment.domain; + +import demo.event.Event; + +/** + * 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}. + * + * @author kbastani + */ +public class PaymentEvent extends Event { + + private Long eventId; + private PaymentEventType type; + private Payment payment; + private Long createdAt; + private Long lastModified; + + public PaymentEvent() { + } + + public PaymentEvent(PaymentEventType type) { + this.type = type; + } + + public PaymentEvent(PaymentEventType type, Payment payment) { + this.type = type; + this.payment = payment; + } + + @Override + public Long getEventId() { + return eventId; + } + + @Override + public void setEventId(Long id) { + eventId = id; + } + + @Override + public PaymentEventType getType() { + return type; + } + + @Override + public void setType(PaymentEventType type) { + this.type = type; + } + + @Override + public Payment getEntity() { + return payment; + } + + @Override + public void setEntity(Payment entity) { + this.payment = entity; + } + + @Override + public Long getCreatedAt() { + return createdAt; + } + + @Override + public void setCreatedAt(Long createdAt) { + this.createdAt = createdAt; + } + + @Override + public Long getLastModified() { + return lastModified; + } + + @Override + public void setLastModified(Long lastModified) { + this.lastModified = lastModified; + } +} + diff --git a/order/order-web/src/main/java/demo/payment/domain/PaymentEventType.java b/order/order-web/src/main/java/demo/payment/domain/PaymentEventType.java new file mode 100644 index 0000000..f04efc5 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/domain/PaymentEventType.java @@ -0,0 +1,16 @@ +package demo.payment.domain; + +/** + * 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, + ORDER_CONNECTED, + PAYMENT_PENDING, + PAYMENT_PROCESSED, + PAYMENT_FAILED, + PAYMENT_SUCCEEDED +} diff --git a/order/order-web/src/main/java/demo/payment/domain/PaymentModule.java b/order/order-web/src/main/java/demo/payment/domain/PaymentModule.java new file mode 100644 index 0000000..c47d19a --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/domain/PaymentModule.java @@ -0,0 +1,35 @@ +package demo.payment.domain; + +import demo.domain.Module; +import demo.event.EventService; +import demo.order.event.OrderEvent; + +@org.springframework.stereotype.Service +public class PaymentModule extends Module { + + private final PaymentService paymentService; + private final EventService eventService; + + public PaymentModule(PaymentService paymentService, EventService eventService) { + this.paymentService = paymentService; + this.eventService = eventService; + } + + public PaymentService getPaymentService() { + return paymentService; + } + + public EventService getEventService() { + return eventService; + } + + @Override + public PaymentService getDefaultService() { + return paymentService; + } + + @Override + public EventService getDefaultEventService() { + return eventService; + } +} diff --git a/order/order-web/src/main/java/demo/payment/domain/PaymentService.java b/order/order-web/src/main/java/demo/payment/domain/PaymentService.java new file mode 100644 index 0000000..701c285 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/domain/PaymentService.java @@ -0,0 +1,47 @@ +package demo.payment.domain; + +import demo.domain.Service; +import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.UriTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.http.RequestEntity; +import org.springframework.web.client.RestTemplate; + +@org.springframework.stereotype.Service +public class PaymentService extends Service { + + private RestTemplate restTemplate; + + public PaymentService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @Override + public Payment get(Long paymentId) { + return restTemplate.getForObject(new UriTemplate("http://payment-web/v1/payments/{id}") + .with("id", TemplateVariable.VariableType.PATH_VARIABLE) + .expand(paymentId), Payment.class); + } + + @Override + public Payment create(Payment payment) { + return restTemplate.postForObject(new UriTemplate("http://payment-web/v1/payments").expand(), + payment, Payment.class); + } + + @Override + public Payment update(Payment payment) { + return restTemplate.exchange(new RequestEntity<>(payment, HttpMethod.PUT, new UriTemplate + ("http://payment-web/v1/payments/{id}").with("id", TemplateVariable.VariableType.PATH_VARIABLE) + .expand(payment.getIdentity())), Payment.class) + .getBody(); + } + + @Override + public boolean delete(Long paymentId) { + restTemplate.delete(new UriTemplate("http://payment-web/v1/payments/{id}").with("id", TemplateVariable + .VariableType.PATH_VARIABLE) + .expand(paymentId)); + return true; + } +} diff --git a/order/order-web/src/main/java/demo/payment/domain/PaymentStatus.java b/order/order-web/src/main/java/demo/payment/domain/PaymentStatus.java index 1ceedff..dead7ae 100644 --- a/order/order-web/src/main/java/demo/payment/domain/PaymentStatus.java +++ b/order/order-web/src/main/java/demo/payment/domain/PaymentStatus.java @@ -2,6 +2,7 @@ package demo.payment.domain; public enum PaymentStatus { PAYMENT_CREATED, + ORDER_CONNECTED, PAYMENT_PENDING, PAYMENT_PROCESSED, PAYMENT_FAILED, diff --git a/order/order-web/src/main/java/demo/payment/domain/Payments.java b/order/order-web/src/main/java/demo/payment/domain/Payments.java new file mode 100644 index 0000000..3df7e83 --- /dev/null +++ b/order/order-web/src/main/java/demo/payment/domain/Payments.java @@ -0,0 +1,24 @@ +package demo.payment.domain; + +import demo.order.domain.Order; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resources; + +public class Payments extends Resources { + + /** + * Creates an empty {@link Resources} instance. + */ + public Payments() { + } + + /** + * 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}. + */ + public Payments(Iterable content, Link... links) { + super(content, links); + } +} 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 f02d3bd..08406f4 100644 --- a/order/order-worker/src/main/java/demo/config/StateMachineConfig.java +++ b/order/order-worker/src/main/java/demo/config/StateMachineConfig.java @@ -135,17 +135,11 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new OrderCreated(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON @@ -183,7 +177,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new PaymentPending(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON @@ -200,7 +194,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new ReservationPending(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON @@ -217,7 +211,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new PaymentFailed(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON @@ -231,19 +225,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter 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(); - })); + return context -> applyEvent(context, new PaymentSucceeded(context)); } @Bean @@ -251,15 +233,16 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new PaymentConnected(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + + // Create a traverson for the root order Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON ); - return traverson.follow("self") - .toEntity(Order.class) - .getBody(); + // Traverse to the process payment link + return traverson.follow("self", "commands", "processPayment") + .toObject(Order.class); })); } @@ -268,7 +251,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new PaymentCreated(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson paymentResource = new Traverson( URI.create(event.getLink("payment").getHref()), MediaTypes.HAL_JSON @@ -307,7 +290,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new ReservationSucceeded(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON @@ -324,7 +307,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new ReservationSucceeded(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON @@ -341,7 +324,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter applyEvent(context, new ReservationFailed(context, event -> { log.info(event.getType() + ": " + event.getLink("order").getHref()); - // Get the account resource for the event + // Get the order resource for the event Traverson traverson = new Traverson( URI.create(event.getLink("order").getHref()), MediaTypes.HAL_JSON diff --git a/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java b/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java index 95073b0..ee6f66f 100644 --- a/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java +++ b/order/order-worker/src/main/java/demo/function/PaymentSucceeded.java @@ -5,14 +5,23 @@ import demo.order.event.OrderEventType; import demo.order.domain.Order; import demo.order.domain.OrderStatus; import org.apache.log4j.Logger; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.client.Traverson; +import org.springframework.http.RequestEntity; import org.springframework.statemachine.StateContext; +import org.springframework.web.client.RestTemplate; +import java.net.URI; import java.util.function.Function; public class PaymentSucceeded extends OrderFunction { final private Logger log = Logger.getLogger(PaymentSucceeded.class); + public PaymentSucceeded(StateContext context) { + this(context, null); + } + public PaymentSucceeded(StateContext context, Function lambda) { super(context, lambda); } @@ -25,7 +34,50 @@ public class PaymentSucceeded extends OrderFunction { */ @Override public Order apply(OrderEvent event) { + Order order; + log.info("Executing workflow for payment succeeded..."); - return super.apply(event); + + // Create a traverson for the root order + Traverson traverson = new Traverson( + URI.create(event.getLink("order").getHref()), + MediaTypes.HAL_JSON + ); + + // Get the order resource attached to the event + order = traverson.follow("self") + .toEntity(Order.class) + .getBody(); + + // Set the order to a pending state + order = setOrderPaymentSucceededStatus(event, order); + + context.getExtendedState().getVariables().put("order", order); + + return order; + } + + /** + * Set the {@link Order} resource to a payment succeeded state. + * + * @param event is the {@link OrderEvent} for this context + * @param order is the {@link Order} attached to the {@link OrderEvent} resource + * @return an {@link Order} with its updated state set to pending + */ + private Order setOrderPaymentSucceededStatus(OrderEvent event, Order order) { + // Set the account status to pending + order.setStatus(OrderStatus.PAYMENT_SUCCEEDED); + RestTemplate restTemplate = new RestTemplate(); + + // Create a new request entity + RequestEntity requestEntity = RequestEntity.put( + URI.create(event.getLink("order").getHref())) + .contentType(MediaTypes.HAL_JSON) + .body(order); + + // Update the account entity's status + order = restTemplate.exchange(requestEntity, Order.class).getBody(); + + return order; } } diff --git a/order/order-worker/src/main/java/demo/order/domain/Order.java b/order/order-worker/src/main/java/demo/order/domain/Order.java index 8f10a8f..b393c0a 100644 --- a/order/order-worker/src/main/java/demo/order/domain/Order.java +++ b/order/order-worker/src/main/java/demo/order/domain/Order.java @@ -12,6 +12,10 @@ public class Order extends AbstractEntity { private Long id; + private Long accountId; + + private Long paymentId; + private OrderStatus status; private Set events = new HashSet<>(); @@ -68,6 +72,22 @@ public class Order extends AbstractEntity { lineItems.add(lineItem); } + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getPaymentId() { + return paymentId; + } + + public void setPaymentId(Long paymentId) { + this.paymentId = paymentId; + } + /** * Returns the {@link Link} with a rel of {@link Link#REL_SELF}. */ diff --git a/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java b/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java index 6660bc4..8c08782 100644 --- a/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java +++ b/payment/payment-web/src/main/java/demo/payment/action/ConnectOrder.java @@ -8,13 +8,19 @@ import demo.payment.domain.PaymentStatus; import demo.payment.event.PaymentEvent; import demo.payment.event.PaymentEventType; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; -import java.util.function.BiConsumer; +import java.util.function.BiFunction; @Service public class ConnectOrder extends Action { - public BiConsumer getConsumer() { + public BiFunction getFunction() { return (payment, orderId) -> { + Assert.isTrue(payment + .getStatus() == PaymentStatus.PAYMENT_CREATED, "Payment has already been connected to an order"); + + Payment result; + PaymentService paymentService = payment.getModule(PaymentModule.class) .getDefaultService(); @@ -23,8 +29,17 @@ public class ConnectOrder extends Action { payment.setStatus(PaymentStatus.ORDER_CONNECTED); payment = paymentService.update(payment); - // Trigger the payment connected - payment.sendAsyncEvent(new PaymentEvent(PaymentEventType.ORDER_CONNECTED, payment)); + try { + // Trigger the payment connected + result = payment.sendEvent(new PaymentEvent(PaymentEventType.ORDER_CONNECTED, payment)).getEntity(); + } catch (IllegalStateException ex) { + payment.setStatus(PaymentStatus.PAYMENT_CREATED); + payment.setOrderId(null); + paymentService.update(payment); + throw ex; + } + + return result; }; } } diff --git a/payment/payment-web/src/main/java/demo/payment/action/ProcessPayment.java b/payment/payment-web/src/main/java/demo/payment/action/ProcessPayment.java index 1b88d7d..4abad15 100644 --- a/payment/payment-web/src/main/java/demo/payment/action/ProcessPayment.java +++ b/payment/payment-web/src/main/java/demo/payment/action/ProcessPayment.java @@ -2,14 +2,31 @@ package demo.payment.action; import demo.domain.Action; import demo.payment.domain.Payment; +import demo.payment.domain.PaymentModule; +import demo.payment.domain.PaymentService; import demo.payment.domain.PaymentStatus; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import java.util.Arrays; import java.util.function.Consumer; @Service public class ProcessPayment extends Action { public Consumer getConsumer() { - return payment -> payment.setStatus(PaymentStatus.PAYMENT_PROCESSED); + return payment -> { + // Validations + Assert.isTrue(!Arrays.asList(PaymentStatus.PAYMENT_SUCCEEDED, + PaymentStatus.PAYMENT_PENDING, + PaymentStatus.PAYMENT_FAILED).contains(payment.getStatus()), "Payment has already been processed"); + Assert.isTrue(payment.getStatus() == PaymentStatus.ORDER_CONNECTED, + "Payment must be connected to an order"); + + PaymentService paymentService = payment.getModule(PaymentModule.class) + .getDefaultService(); + + payment.setStatus(PaymentStatus.PAYMENT_PROCESSED); + paymentService.update(payment); + }; } } diff --git a/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java b/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java index b21655e..3e66f22 100644 --- a/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java +++ b/payment/payment-web/src/main/java/demo/payment/controller/PaymentController.java @@ -214,8 +214,8 @@ public class PaymentController { } // Add remote payment link - if (payment.getOrderId() != null) { - Link result = getRemoteLink("order-web", "/v1/orders/{id}", payment.getOrderId(), "order "); + if (payment.getOrderId() != null && !payment.hasLink("order")) { + Link result = getRemoteLink("order-web", "/v1/orders/{id}", payment.getOrderId(), "order"); if (result != null) payment.add(result); } diff --git a/payment/payment-web/src/main/java/demo/payment/domain/Payment.java b/payment/payment-web/src/main/java/demo/payment/domain/Payment.java index 6a449c1..df442c7 100644 --- a/payment/payment-web/src/main/java/demo/payment/domain/Payment.java +++ b/payment/payment-web/src/main/java/demo/payment/domain/Payment.java @@ -90,11 +90,9 @@ public class Payment extends AbstractEntity { @Command(method = "connectOrder", controller = PaymentController.class) public Payment connectOrder(Long orderId) { - getAction(ConnectOrder.class) - .getConsumer() - .accept(this, orderId); - - return this; + return getAction(ConnectOrder.class) + .getFunction() + .apply(this, orderId); } @Command(method = "processPayment", controller = PaymentController.class) diff --git a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Action.java b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Action.java index 16f4854..8f09cf6 100644 --- a/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Action.java +++ b/spring-boot-starters/spring-boot-starter-data-events/src/main/java/demo/domain/Action.java @@ -1,10 +1,14 @@ package demo.domain; +import org.apache.log4j.Logger; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; +import java.util.Map; +import java.util.function.Consumer; + /** * An {@link Action} is a reference of a method. A function contains an address to the location of a method. A function * may contain meta-data that describes the inputs and outputs of a method. An action invokes a method annotated with @@ -14,10 +18,19 @@ import org.springframework.stereotype.Component; */ @Component public abstract class Action implements ApplicationContextAware { + private final Logger log = Logger.getLogger(Action.class); private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + + protected Consumer onSuccess(Map context) { + return a -> {}; + } + + protected Consumer onError(Map context) { + return a -> {}; + } }