Order and Payment
This commit is contained in:
71
payment/payment-web/pom.xml
Normal file
71
payment/payment-web/pom.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>payment-web</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>payment-web</name>
|
||||
|
||||
<parent>
|
||||
<groupId>org.kbastani</groupId>
|
||||
<artifactId>payment</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-hateoas</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-integration</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
13
payment/payment-web/src/main/java/demo/config/JpaConfig.java
Normal file
13
payment/payment-web/src/main/java/demo/config/JpaConfig.java
Normal file
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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<HttpMessageConverter<?>> 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));
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package demo.event;
|
||||
|
||||
public enum ConsistencyModel {
|
||||
BASE,
|
||||
ACID
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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<PaymentEvent, Long> {
|
||||
Page<PaymentEvent> findPaymentEventsByPaymentId(@Param("paymentId") Long paymentId, Pageable pageable);
|
||||
}
|
||||
214
payment/payment-web/src/main/java/demo/event/EventService.java
Normal file
214
payment/payment-web/src/main/java/demo/event/EventService.java
Normal file
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<Resource<PaymentEvent>> 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<PaymentEvent> 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<PaymentEvent> 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<PaymentEvent> getPaymentEventResource(PaymentEvent event) {
|
||||
return new Resource<PaymentEvent>(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;
|
||||
}
|
||||
}
|
||||
@@ -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}.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<PaymentEvent> 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<PaymentEvent> 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<PaymentEvent> 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;
|
||||
}
|
||||
}
|
||||
106
payment/payment-web/src/main/java/demo/payment/Payment.java
Normal file
106
payment/payment-web/src/main/java/demo/payment/Payment.java
Normal file
@@ -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<PaymentEvent> 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<PaymentEvent> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
public void setEvents(Set<PaymentEvent> 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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<Payment> getPaymentResource(Long id) {
|
||||
Resource<Payment> 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<Payment> 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<Payment> 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<PaymentEvent> appendEventResource(Long paymentId, PaymentEvent event) {
|
||||
Resource<PaymentEvent> 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<Payment> 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<Payment> getPaymentResource(Payment payment) {
|
||||
Resource<Payment> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package demo.payment;
|
||||
|
||||
public enum PaymentMethod {
|
||||
CREDIT_CARD
|
||||
}
|
||||
@@ -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, Long> {
|
||||
|
||||
Payment findPaymentByOrderId(@Param("orderId") Long orderId);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
17
payment/payment-web/src/main/resources/application.yml
Normal file
17
payment/payment-web/src/main/resources/application.yml
Normal file
@@ -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
|
||||
4
payment/payment-web/src/main/resources/bootstrap.yml
Normal file
4
payment/payment-web/src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
spring:
|
||||
application:
|
||||
name: payment-web
|
||||
---
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
1
payment/payment-web/src/test/resources/data-h2.sql
Normal file
1
payment/payment-web/src/test/resources/data-h2.sql
Normal file
@@ -0,0 +1 @@
|
||||
INSERT INTO PAYMENT(ID, FIRST_NAME, LAST_NAME, EMAIL) values (1, 'John', 'Doe', 'john.doe@example.com');
|
||||
Reference in New Issue
Block a user