Finish payment workflow

This commit is contained in:
Kenny Bastani
2017-01-03 18:10:13 -05:00
parent 7644f5beba
commit 7ea6969792
25 changed files with 627 additions and 168 deletions

View File

@@ -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<Account> 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<Account> 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"));
}

View File

@@ -61,6 +61,11 @@
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>${json-path.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>

View File

@@ -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<Order> {
private final Logger log = Logger.getLogger(this.getClass());
public BiConsumer<Order, Long> getConsumer() {
public BiFunction<Order, Long, Order> 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);
try {
// Trigger the account connected event
order.sendAsyncEvent(new OrderEvent(OrderEventType.ACCOUNT_CONNECTED, order));
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;
};
}
}

View File

@@ -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<Order> {
public BiConsumer<Order, Long> getConsumer() {
private final Logger log = Logger.getLogger(this.getClass());
public BiFunction<Order, Long, Order> 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;
};
}
}

View File

@@ -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<Order> {
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<Order> getConsumer() {
public Function<Order, Order> 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<Resource<Payment>> 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);
try {
OrderEvent event = new OrderEvent(OrderEventType.PAYMENT_CREATED, order);
event.add(paymentResource.getLink("self")
.withRel("payment"));
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;
};
}
}

View File

@@ -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<Order> {
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<Order> 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)

View File

@@ -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<Order> {
public Consumer<Order> getConsumer() {
return (order) -> {};
private final Logger log = Logger.getLogger(this.getClass());
public Function<Order, Order> 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;
};
}
}

View File

@@ -212,23 +212,27 @@ public class OrderController {
* @return is a hypermedia enriched resource for the supplied {@link Order} entity
*/
private Resource<Order> getOrderResource(Order order) {
if(order == null) return null;
if (order == null) return null;
if (order.getLink("commands") == null) {
// Add command link
order.add(linkBuilder("getCommands", order.getIdentity()).withRel("commands"));
}
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);

View File

@@ -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<OrderEvent, Long> {
@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<OrderEvent, Long> {
.sum();
}
@JsonIgnore
public Payment getPayment() {
Payment result = null;
if (paymentId != null)
result = getModule(OrderModule.class).getPaymentService().get(paymentId);
return result;
}
@Override
@SuppressWarnings("unchecked")
public <T extends Module<A>, A extends Aggregate<OrderEvent, Long>> T getModule() throws
IllegalArgumentException {
OrderModule orderModule = getModule(OrderModule.class);
return (T) orderModule;
}
/**
* Returns the {@link Link} with a rel of {@link Link#REL_SELF}.
*/

View File

@@ -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<Order> {
private final OrderService orderService;
private final PaymentService paymentService;
private final EventService<OrderEvent, Long> eventService;
public OrderModule(OrderService orderService, EventService<OrderEvent, Long> eventService) {
public OrderModule(OrderService orderService, PaymentService paymentService, EventService<OrderEvent, Long>
eventService) {
this.orderService = orderService;
this.paymentService = paymentService;
this.eventService = eventService;
}
@@ -32,4 +36,8 @@ public class OrderModule extends Module<Order> {
public EventService<OrderEvent, Long> getDefaultEventService() {
return eventService;
}
public PaymentService getPaymentService() {
return paymentService;
}
}

View File

@@ -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<PaymentEvent, Long> {
private Long id;
private List<PaymentEvent> 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<PaymentEvent> 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 <T extends Module<A>, A extends Aggregate<PaymentEvent, Long>> 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();

View File

@@ -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<Payment, PaymentEventType, Long> {
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;
}
}

View File

@@ -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
}

View File

@@ -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<Payment> {
private final PaymentService paymentService;
private final EventService<OrderEvent, Long> eventService;
public PaymentModule(PaymentService paymentService, EventService<OrderEvent, Long> eventService) {
this.paymentService = paymentService;
this.eventService = eventService;
}
public PaymentService getPaymentService() {
return paymentService;
}
public EventService<OrderEvent, Long> getEventService() {
return eventService;
}
@Override
public PaymentService getDefaultService() {
return paymentService;
}
@Override
public EventService<OrderEvent, Long> getDefaultEventService() {
return eventService;
}
}

View File

@@ -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<Payment, Long> {
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;
}
}

View File

@@ -2,6 +2,7 @@ package demo.payment.domain;
public enum PaymentStatus {
PAYMENT_CREATED,
ORDER_CONNECTED,
PAYMENT_PENDING,
PAYMENT_PROCESSED,
PAYMENT_FAILED,

View File

@@ -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<Order> {
/**
* 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<Order> content, Link... links) {
super(content, links);
}
}

View File

@@ -135,17 +135,11 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderS
.and()
.withExternal()
.source(OrderStatus.PAYMENT_CREATED)
.target(OrderStatus.PAYMENT_CONNECTED)
.target(OrderStatus.PAYMENT_PENDING)
.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)
@@ -166,7 +160,7 @@ public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderS
return context -> 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<OrderS
return context -> 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<OrderS
return context -> 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<OrderS
return context -> 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<OrderS
@Bean
public Action<OrderStatus, OrderEventType> 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<OrderS
return context -> 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<OrderS
return context -> 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<OrderS
return context -> 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<OrderS
return context -> 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<OrderS
return context -> 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

View File

@@ -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<OrderStatus, OrderEventType> context) {
this(context, null);
}
public PaymentSucceeded(StateContext<OrderStatus, OrderEventType> context, Function<OrderEvent, Order> 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<Order> 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;
}
}

View File

@@ -12,6 +12,10 @@ public class Order extends AbstractEntity {
private Long id;
private Long accountId;
private Long paymentId;
private OrderStatus status;
private Set<OrderEvent> 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}.
*/

View File

@@ -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<Payment> {
public BiConsumer<Payment, Long> getConsumer() {
public BiFunction<Payment, Long, Payment> 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> {
payment.setStatus(PaymentStatus.ORDER_CONNECTED);
payment = paymentService.update(payment);
try {
// Trigger the payment connected
payment.sendAsyncEvent(new PaymentEvent(PaymentEventType.ORDER_CONNECTED, payment));
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;
};
}
}

View File

@@ -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<Payment> {
public Consumer<Payment> 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);
};
}
}

View File

@@ -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);
}

View File

@@ -90,11 +90,9 @@ public class Payment extends AbstractEntity<PaymentEvent, Long> {
@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)

View File

@@ -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<A extends Aggregate> 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<A> onSuccess(Map<String, Object> context) {
return a -> {};
}
protected Consumer<A> onError(Map<String, Object> context) {
return a -> {};
}
}