54 Commits

Author SHA1 Message Date
Chris Richardson
8ae6db7229 Merge branch 'master' into wip-eventuate-client-java
Conflicts:
	java-spring/monolithic-service/build.gradle
	java-spring/monolithic-service/src/main/java/net/chrisrichardson/eventstore/javaexamples/banking/web/BankingWebConfiguration.java
2016-09-06 21:51:17 -07:00
Chris Richardson
c03a5fed8d Merge pull request #36 from dartpopikyardo/wip-customer
UI updates, backend transaction history fixes
2016-09-06 20:49:04 -07:00
dartpopikyardo
ee45163f2c merged with shopcookeat/private-event-sourcing-examples wip-customer 2016-09-06 21:53:05 +03:00
Andrew Revinsky (DART)
e6bf638b4b Transfers history - correct statuses and calculation 2016-09-06 20:29:47 +03:00
dartpopikyardo
d6f60101db - updated AccountInfoUpdateService: fixed simultaneous upsert overrides 2016-09-06 19:06:40 +03:00
Andrew Revinsky (DART)
e54577d656 REST endpoints should return an JSON object, not an Array. #14 2016-09-06 02:46:19 +03:00
dartpopikyardo
bae00f6bd7 private-event-sourcing-examples-8 GatewayController ignores http status code returned by destination server
- added http status and headers to GatewayController's response
2016-09-05 22:53:56 +03:00
dartpopikyardo
68dca23a6b updated AccountInfoUpdateService: changed addTransaction saving-to-mongo logic 2016-09-05 22:28:19 +03:00
dartpopikyardo
19c9f88a7f wip issue #8 2016-09-05 15:17:28 +03:00
dartpopikyardo
9b6956b8df wip issue #8 2016-09-05 14:46:48 +03:00
dartpopikyardo
e2de325df2 wip issue #8 2016-09-05 14:31:14 +03:00
dartpopikyardo
c9fa916cdd fixed issue #22
fixed issue #35
2016-09-05 12:05:48 +03:00
dartpopikyardo
f79ebb4d18 Merge remote-tracking branch 'remotes/dartandrevinsky/wip-customer' into wip-customer 2016-09-02 23:53:11 +03:00
dartpopikyardo
8f2fc83a34 wip issue #22 - fixed api gateway controller's request mapping 2016-09-02 23:52:29 +03:00
Andrew Revinsky (DART)
05f2f309e7 Transaction history - step 1 2016-09-02 23:14:52 +03:00
dartpopikyardo
6cf774da2a wip issue #22 2016-09-02 15:39:03 +03:00
dartpopikyardo
3c2e9d374c wip issue #22 2016-09-02 15:32:55 +03:00
dartpopikyardo
55cb34ef4f fix issue #22 2016-09-02 15:11:44 +03:00
dartpopikyardo
dde554e442 reverted back AccountInfo and AccountInfoUpdateService changes 2016-09-02 11:53:50 +03:00
Chris Richardson
fd75779093 Merge pull request #33 from shopcookeat/revert-32-wip-customer
Revert "wip-customer small issues fixes"
2016-09-01 16:12:28 -07:00
Chris Richardson
c8291bec71 Revert "wip-customer small issues fixes" 2016-09-01 16:11:53 -07:00
Chris Richardson
1005c47d83 Merge pull request #32 from dartpopikyardo/wip-customer
wip-customer small issues fixes
2016-09-01 15:53:08 -07:00
dartpopikyardo
47e9053285 wip on issue #22 2016-09-02 01:45:30 +03:00
dartpopikyardo
f76912a6cf fix issue #13,
fix issue #14,
fix issue #15,
fix issue #17
2016-09-02 01:24:28 +03:00
dartpopikyardo
625ea6007e fix issue #7,
fix issue #8,
fix issue #9,
fix issue #11
2016-09-01 22:34:26 +03:00
Chris Richardson
6fae59fdeb Merge pull request #12 from dartandrevinsky/wip-customer
Changes to the app
2016-09-01 08:26:24 -07:00
dartpopikyardo
f846a32d95 fix issue #5,
fix issue #6
2016-09-01 16:36:48 +03:00
dartpopikyardo
2a712017f1 - updated {accountId}/history controller 2016-09-01 16:06:51 +03:00
dartpopikyardo
85613936f4 Merge remote-tracking branch 'remotes/dartandrevinsky/wip-customer' into wip-customer 2016-09-01 09:34:28 +03:00
dartpopikyardo
6f480ad11a fix issue #10 2016-09-01 09:10:03 +03:00
dartpopikyardo
2b0c405378 fixed tests 2016-09-01 08:08:30 +03:00
dartpopikyardo
a5d1e7312c Merge remote-tracking branch 'remotes/upstream/wip-eventuate-local' into wip-customer 2016-09-01 02:39:47 +03:00
Chris Richardson
29d42fda9a Simplified TestUtil, Fixed MongoDB update issue 2016-08-31 13:02:44 -07:00
Andrew Revinsky (DART)
1b53bd9147 Refresh after account creation 2016-08-31 19:40:09 +03:00
Andrew Revinsky (DART)
d1328e4ce8 Merge commit '45bda8e14d32cf016ac5dd105828f2cda073e7aa' into wip-customer
* commit '45bda8e14d32cf016ac5dd105828f2cda073e7aa':
  Fixed misc @Configuration issues and commented out incorrect version check that causing failure in AccountInfoUpdateService
  - fixed test config
  - disable vertx file cache
  -changed path to UI static content
  Merge remote-tracking branch 'remotes/upstream/wip-eventuate-local' into wip-customer
  Upgraded to Eventuate Local 0.2.0.RELEASE
  Bumped version
  Support for Eventuate Local
2016-08-31 11:57:34 +03:00
dartpopikyardo
45bda8e14d Merge last changes from 'remotes/upstream/wip-customer' into wip-customer 2016-08-31 10:06:05 +03:00
Andrew Revinsky (DART)
d9e13ff669 Improve the UI #2 2016-08-31 03:52:35 +03:00
dartpopikyardo
1d14ece9cf - fixed test config 2016-08-30 23:13:32 +03:00
dartpopikyardo
1e13d482a8 - disable vertx file cache 2016-08-30 22:37:55 +03:00
dartpopikyardo
2467099c3e -changed path to UI static content 2016-08-30 19:14:03 +03:00
dartpopikyardo
f4ec33d275 Merge remote-tracking branch 'remotes/upstream/wip-eventuate-local' into wip-customer 2016-08-30 19:07:57 +03:00
dartpopikyardo
a91ade08b1 Merge remote-tracking branch 'remotes/upstream/wip-eventuate-local' into wip-customer 2016-08-30 19:05:44 +03:00
Andrew Revinsky (DART)
fb97767a06 Improve the UI #2 2016-08-26 20:32:10 +03:00
Andrew Revinsky (DART)
1e7234166a Changes to the app 2016-08-26 14:13:50 +03:00
Chris Richardson
d0bdd51406 Fixed typo 2016-08-25 12:15:54 -07:00
Chris Richardson
f4e070e7bd Upgraded to Eventuate Local 0.2.0.RELEASE 2016-08-18 13:53:04 -07:00
Chris Richardson
bd3de1a938 Bumped version 2016-08-06 13:26:08 -07:00
Chris Richardson
f570ccbe90 Support for Eventuate Local 2016-08-05 16:41:16 -07:00
Chris Richardson
e488df3d06 Fixed typo 2016-06-10 16:45:02 -07:00
Chris Richardson
4f11433390 Merge branch 'master' of github.com:cer/event-sourcing-examples 2016-06-10 16:43:32 -07:00
Chris Richardson
b573027fc2 Added Swagger to monolithic deployment, Added mongodb-cli.sh 2016-06-10 16:43:14 -07:00
Chris Richardson
3117f12402 Fixed typo 2016-06-06 20:48:58 -07:00
Chris Richardson
5b029d8307 Merge branch 'master' of github.com:cer/event-sourcing-examples 2016-05-31 15:08:46 -07:00
Chris Richardson
6d8376cfd5 Use specific Java 8 Docker image 2016-05-31 15:08:34 -07:00
118 changed files with 66401 additions and 821 deletions

View File

@@ -1,7 +1,7 @@
#Event-Sourcing+CQRS example application
This example application is the money transfer application described in my talk [Building and deploying microservices with event sourcing, CQRS and Docker](http://plainoldobjects.com/presentations/building-and-deploying-microservices-with-event-sourcing-cqrs-and-docker/).
This talk describe a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
This talk describes a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
event sourcing (ES) and command query responsibility segregation (CQRS).
Applications consist of loosely coupled components that communicate using events.
These components can be deployed either as separate services or packaged as a monolithic application for simplified development and testing.
@@ -94,9 +94,12 @@ First, you need to tell the query side code how to connect to MongoDB:
```
[Docker Compose](https://docs.docker.com/compose/) is a great way to run MongoDB.
You can run the `docker-compose up -d mongodb` to run MongoDB.
You can run the `docker-compose up -d mongodb` to run MongoDB and then set `SPRING_DATA_MONGODB_URI` as follows:
```
export SPRING_DATA_MONGODB_URI=mongodb://$(docker-machine ip default)/yourdb
```
Second, some of the tests in accounts-command-side-service, transactions-command-side-service, accounts-query-side-service and e2e-test need you need to set some environment variables that tell them how to connect to the Event Store server.
Second, some of the tests in accounts-command-side-service, transactions-command-side-service, accounts-query-side-service and e2e-test require you to set some environment variables that tell them how to connect to the Event Store server.
But don't worry.
The build is configured to ignore failures for those projects.
@@ -117,6 +120,15 @@ Simply use this command:
java -jar monolithic-service/build/libs/monolithic-service.jar
```
This will start the service running on port 8080 (you can change using the --server.port=9999 option).
Once the service has started you can open the Swagger UI: http://localhost:8080/swagger-ui.html.
You can then:
1. Create two accounts (save the account ids)
2. Create a money transfer
3. View the updated account balances
## Running the microservices
The other option is to run the services separately.

View File

@@ -6,6 +6,12 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
DOCKER_COMPOSE="docker-compose -p event-sourcing-examples"
if [ "$1" = "-f" ] ; then
shift;
DOCKER_COMPOSE="$DOCKER_COMPOSE -f ${1?}"
shift
fi
if [ "$1" = "--use-existing" ] ; then
shift;
else
@@ -13,6 +19,13 @@ else
${DOCKER_COMPOSE?} rm -v --force
fi
NO_RM=false
if [ "$1" = "--no-rm" ] ; then
NO_RM=true
shift
fi
${DOCKER_COMPOSE?} up -d mongodb
if [ -z "$DOCKER_HOST_IP" ] ; then
@@ -47,5 +60,7 @@ set -e
./gradlew -a $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false
${DOCKER_COMPOSE?} stop
${DOCKER_COMPOSE?} rm -v --force
if [ $NO_RM = false ] ; then
${DOCKER_COMPOSE?} stop
${DOCKER_COMPOSE?} rm -v --force
fi

View File

@@ -7,19 +7,19 @@ This application consists of three microservices:
* Account Service - the command side business logic for Accounts
* Money Transfer Service - the command side business logic for Money Transfers
* Query service - query side implementation of a MongoDB-based, denormalized view of Accounts and MoneyTransfers
The Account Service consists of the following modules:
* accounts-command-side-backend - the Account aggregate
* accounts-command-side-web - a REST API for creating and retrieving Accounts
* accounts-command-side-service - a standalone microservice
The Money Transfer Service consists of the following modules:
* transactions-command-side-backend - the MoneyTransfer aggregate
* transactions-command-side-web - a REST API for creating and retrieving Money Transfers
* transactions-command-side-service - a standalone microservice
The Query Service consists the following modules:
* accounts-query-side-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
@@ -28,10 +28,8 @@ The Query Service consists the following modules:
# Deploying the application
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simpified integration testing.
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simplified integration testing.
The three services can also be packaged as a single monolithic web application in order to be used with the embedded Event Store:
* monolithic-service - all-in-one, monolithic packaging of the application

View File

@@ -1,5 +1,6 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: EventuateDependencyPlugin
apply plugin: 'spring-boot'
@@ -10,9 +11,6 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}

View File

@@ -25,7 +25,7 @@ public class AccountsCommandSideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Autowired

View File

@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/accounts")
@RequestMapping("/api/accounts")
public class AccountController {
private AccountService accountService;

View File

@@ -35,7 +35,7 @@ public class AccountControllerIntegrationTest {
@Test
public void shouldCreateAccount() throws Exception {
mockMvc.perform(post("/accounts")
mockMvc.perform(post("/api/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"customerId\" : \"00000000-00000000\", \"initialBalance\" : 500}")
.accept(MediaType.APPLICATION_JSON))
@@ -44,7 +44,7 @@ public class AccountControllerIntegrationTest {
@Test
public void shouldRejectBadRequest() throws Exception {
mockMvc.perform(post("/accounts")
mockMvc.perform(post("/api/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"initialBalanceXXX\" : 500}")
.accept(MediaType.APPLICATION_JSON))

View File

@@ -1,18 +0,0 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
public class AccountChangeInfo {
private String changeId;
private String transactionId;
private String transactionType;
private long amount;
private long balanceDelta;
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
this.changeId = changeId;
this.transactionId = transactionId;
this.transactionType = transactionType;
this.amount = amount;
this.balanceDelta = balanceDelta;
}
}

View File

@@ -1,8 +1,10 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import java.util.List;
import java.util.*;
/**
* Created by cer on 11/21/14.
@@ -15,13 +17,19 @@ public class AccountInfo {
private String description;
private long balance;
private List<AccountChangeInfo> changes;
private List<AccountTransactionInfo> transactions;
private Map<String, AccountTransactionInfo> transactions;
private Map<String, TransferState> transferStates;
private String version;
private Date date;
private AccountInfo() {
}
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, List<AccountTransactionInfo> transactions, String version) {
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version) {
this(id, customerId, title, description, balance, changes, transactions, version, new Date());
}
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version, Date date) {
this.id = id;
this.customerId = customerId;
@@ -31,6 +39,7 @@ public class AccountInfo {
this.changes = changes;
this.transactions = transactions;
this.version = version;
this.date = date;
}
public String getId() {
@@ -54,14 +63,26 @@ public class AccountInfo {
}
public List<AccountChangeInfo> getChanges() {
return changes;
return changes == null ? Collections.EMPTY_LIST : changes;
}
public List<AccountTransactionInfo> getTransactions() {
return transactions;
return transactions == null ? Collections.EMPTY_LIST : new ArrayList<>(transactions.values());
}
public String getVersion() {
return version;
}
public Date getDate() {
return date;
}
public Map<String, TransferState> getTransferStates() {
return transferStates;
}
public void setTransferStates(Map<String, TransferState> transferStates) {
this.transferStates = transferStates;
}
}

View File

@@ -1,15 +1,18 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import com.mongodb.WriteResult;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
import static org.springframework.data.mongodb.core.query.Criteria.where;
@@ -28,29 +31,33 @@ public class AccountInfoUpdateService {
public void create(String accountId, String customerId, String title, BigDecimal initialBalance, String description, String version) {
try {
accountInfoRepository.save(new AccountInfo(
accountId,
customerId,
title,
description,
toIntegerRepr(initialBalance),
Collections.<AccountChangeInfo>emptyList(),
Collections.<AccountTransactionInfo>emptyList(),
version));
AccountChangeInfo ci = new AccountChangeInfo();
ci.setAmount(toIntegerRepr(initialBalance));
WriteResult x = mongoTemplate.upsert(new Query(where("id").is(accountId).and("version").exists(false)),
new Update()
.set("customerId", customerId)
.set("title", title)
.set("description", description)
.set("balance", toIntegerRepr(initialBalance))
.push("changes", ci)
.set("date", new Date())
.set("version", version),
AccountInfo.class);
logger.info("Saved in mongo");
} catch (DuplicateKeyException t) {
logger.warn("When saving ", t);
} catch (Throwable t) {
logger.error("Error during saving: ");
logger.error("Error during saving: ", t);
throw new RuntimeException(t);
}
}
public void addTransaction(String eventId, String fromAccountId, AccountTransactionInfo ti) {
mongoTemplate.updateMulti(new Query(where("id").is(fromAccountId)), /* wrong .and("version").lt(eventId) */
public void addTransaction(String accountId, AccountTransactionInfo ti) {
mongoTemplate.upsert(new Query(where("id").is(accountId)),
new Update().
push("transactions", ti).
set("version", eventId),
set("transactions." + ti.getTransactionId(), ti),
AccountInfo.class);
}
@@ -64,5 +71,10 @@ public class AccountInfoUpdateService {
AccountInfo.class);
}
public void updateTransactionStatus(String accountId, String transactionId, TransferState status) {
mongoTemplate.upsert(new Query(where("id").is(accountId)),
new Update().
set("transferStates." + transactionId, status),
AccountInfo.class);
}
}

View File

@@ -1,9 +1,6 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import io.eventuate.CompletableFutureUtil;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class AccountQueryService {
@@ -13,15 +10,17 @@ public class AccountQueryService {
this.accountInfoRepository = accountInfoRepository;
}
public CompletableFuture<AccountInfo> findByAccountId(String accountId) {
public AccountInfo findByAccountId(String accountId) {
AccountInfo account = accountInfoRepository.findOne(accountId);
if (account == null)
return CompletableFutureUtil.failedFuture(new AccountNotFoundException(accountId));
throw new AccountNotFoundException(accountId);
else
return CompletableFuture.completedFuture(account);
if(account.getTransferStates()!=null)
account.getTransactions().stream().forEach(ati -> ati.setStatus(account.getTransferStates().get(ati.getTransactionId())));
return account;
}
public CompletableFuture<List<AccountInfo>> findByCustomerId(String customerId) {
return CompletableFuture.completedFuture(accountInfoRepository.findByCustomerId(customerId));
public List<AccountInfo> findByCustomerId(String customerId) {
return accountInfoRepository.findByCustomerId(customerId);
}
}

View File

@@ -3,12 +3,14 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.ac
import io.eventuate.DispatchedEvent;
import io.eventuate.EventHandlerMethod;
import io.eventuate.EventSubscriber;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountChangedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.*;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.CreditRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.FailedDebitRecordedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,20 +59,36 @@ public class AccountQueryWorkflow {
de.getEvent().getDetails().getDate(),
de.getEvent().getDetails().getDescription());
accountInfoUpdateService.addTransaction(eventId, fromAccountId, ti);
accountInfoUpdateService.addTransaction(eventId, toAccountId, ti);
accountInfoUpdateService.addTransaction(fromAccountId, ti);
accountInfoUpdateService.addTransaction(toAccountId, ti);
}
@EventHandlerMethod
public void recordDebit(DispatchedEvent<AccountDebitedEvent> de) {
String accountId = de.getEntityId();
String transactionId = de.getEvent().getTransactionId();
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.DEBITED);
saveChange(de, -1);
}
@EventHandlerMethod
public void recordCredit(DispatchedEvent<AccountCreditedEvent> de) {
String accountId = de.getEntityId();
String transactionId = de.getEvent().getTransactionId();
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.COMPLETED);
saveChange(de, +1);
}
@EventHandlerMethod
public void recordFailed(DispatchedEvent<AccountDebitFailedDueToInsufficientFundsEvent> de) {
String accountId = de.getEntityId();
String transactionId = de.getEvent().getTransactionId();
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS);
}
public <T extends AccountChangedEvent> void saveChange(DispatchedEvent<T> de, int delta) {
String changeId = de.getEventId().asString();
String transactionId = de.getEvent().getTransactionId();

View File

@@ -27,8 +27,6 @@ public class QuerySideAccountConfiguration {
return new AccountQueryService(accountInfoRepository);
}
@Bean
public QuerySideDependencyChecker querysideDependencyChecker(MongoTemplate mongoTemplate) {
return new QuerySideDependencyChecker(mongoTemplate);

View File

@@ -0,0 +1,149 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import io.eventuate.javaclient.spring.jdbc.IdGenerator;
import io.eventuate.javaclient.spring.jdbc.IdGeneratorImpl;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import java.util.Date;
import java.util.concurrent.ExecutionException;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AccountInfoUpdateServiceTest.AccountInfoUpdateServiceTestConfiguration.class)
@IntegrationTest
public class AccountInfoUpdateServiceTest {
@Configuration
@EnableAutoConfiguration
@Import({QuerySideAccountConfiguration.class, EventuateJdbcEventStoreConfiguration.class})
public static class AccountInfoUpdateServiceTestConfiguration {
}
@Autowired
private AccountInfoUpdateService accountInfoUpdateService;
@Autowired
private AccountQueryService accountQueryService;
@Test
public void shouldSaveAccountInfo() throws ExecutionException, InterruptedException {
IdGenerator x = new IdGeneratorImpl();
String accountId = x.genId().asString();
String customerId = x.genId().asString();
String version = x.genId().asString();
String title = "Checking account";
BigDecimal initialBalance = new BigDecimal("1345");
String description = "Some account";
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(accountId, accountInfo.getId());
assertEquals(customerId, accountInfo.getCustomerId());
assertEquals(title, accountInfo.getTitle());
assertEquals(description, accountInfo.getDescription());
assertEquals(initialBalance.longValue() * 100, accountInfo.getBalance());
assertEquals(1, accountInfo.getChanges().size());
assertTrue(accountInfo.getTransactions().isEmpty());
assertEquals(version, accountInfo.getVersion());
String changeId = x.genId().asString();
String transactionId = x.genId().asString();
AccountChangeInfo change = new AccountChangeInfo(changeId, transactionId, AccountCreditedEvent.class.getSimpleName(),
500, +1);
accountInfoUpdateService.updateBalance(accountId, changeId, 500,
change);
accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(initialBalance.add(new BigDecimal(5)).longValue() * 100, accountInfo.getBalance());
assertFalse(accountInfo.getChanges().isEmpty());
assertEquals(change, accountInfo.getChanges().get(1));
String eventId = x.genId().asString();
AccountTransactionInfo ti = new AccountTransactionInfo(transactionId, accountId, accountId, 5, new Date(), "A transfer");
accountInfoUpdateService.addTransaction(accountId, ti);
accountInfo = accountQueryService.findByAccountId(accountId);
assertFalse(accountInfo.getTransactions().isEmpty());
assertEquals(ti, accountInfo.getTransactions().get(0));
}
@Test
public void shouldHandleDuplicateSaveAccountInfo() throws ExecutionException, InterruptedException {
IdGenerator x = new IdGeneratorImpl();
String accountId = x.genId().asString();
String customerId = x.genId().asString();
String version = x.genId().asString();
String title = "Checking account";
BigDecimal initialBalance = new BigDecimal("1345");
String description = "Some account";
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
}
@Test
public void shouldUpdateTransactionStatus() {
IdGenerator x = new IdGeneratorImpl();
String accountId = x.genId().asString();
String customerId = x.genId().asString();
String version = x.genId().asString();
String title = "Checking account";
BigDecimal initialBalance = new BigDecimal("1345");
String description = "Some account";
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
String transactionId = x.genId().asString();
AccountTransactionInfo transactionInfo = new AccountTransactionInfo();
transactionInfo.setTransactionId(transactionId);
transactionInfo.setStatus(TransferState.INITIAL);
accountInfoUpdateService.addTransaction(accountId, transactionInfo);
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(accountId, accountInfo.getId());
assertFalse(accountInfo.getTransactions().isEmpty());
assertEquals(1, accountInfo.getTransactions().size());
assertEquals(TransferState.INITIAL, accountInfo.getTransactions().get(0).getStatus());
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.COMPLETED);
accountInfo = accountQueryService.findByAccountId(accountId);
assertEquals(accountId, accountInfo.getId());
assertFalse(accountInfo.getTransactions().isEmpty());
assertEquals(1, accountInfo.getTransactions().size());
assertEquals(TransferState.COMPLETED, accountInfo.getTransactions().get(0).getStatus());
}
}

View File

@@ -1,5 +1,6 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: EventuateDependencyPlugin
apply plugin: 'spring-boot'
@@ -10,8 +11,6 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile project(":testutil")
testCompile "org.springframework.boot:spring-boot-starter-test"

View File

@@ -30,7 +30,7 @@ public class AccountsQuerySideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Autowired

View File

@@ -4,18 +4,19 @@ import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.acc
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountNotFoundException;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountQueryService;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.GetAccountResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api")
public class AccountQueryController {
private AccountQueryService accountInfoQueryService;
@@ -26,21 +27,35 @@ public class AccountQueryController {
}
@RequestMapping(value = "/accounts/{accountId}", method = RequestMethod.GET)
public CompletableFuture<GetAccountResponse> get(@PathVariable String accountId) {
return accountInfoQueryService.findByAccountId(accountId)
.thenApply(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
public ResponseEntity<GetAccountResponse> get(@PathVariable String accountId) {
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
return ResponseEntity.ok().body(new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
}
@RequestMapping(value = "/accounts", method = RequestMethod.GET)
public CompletableFuture<List<GetAccountResponse>> getAccountsForCustomer(@RequestParam("customerId") String customerId) {
return accountInfoQueryService.findByCustomerId(customerId)
.thenApply(accountInfoList -> accountInfoList.stream().map(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription())).collect(Collectors.toList()));
@RequestMapping(value = "/customers/{customerId}/accounts", method = RequestMethod.GET)
public ResponseEntity<GetAccountsResponse> getAccountsForCustomer(@PathVariable("customerId") String customerId) {
return ResponseEntity.ok().body(
new GetAccountsResponse(
accountInfoQueryService.findByCustomerId(customerId)
.stream()
.map(accountInfo -> new GetAccountResponse(
accountInfo.getId(),
new BigDecimal(accountInfo.getBalance()),
accountInfo.getTitle(),
accountInfo.getDescription()))
.collect(Collectors.toList())
)
);
}
@RequestMapping(value = "/accounts/{accountId}/history", method = RequestMethod.GET)
public CompletableFuture<List<AccountTransactionInfo>> getTransactionsHistory(@PathVariable String accountId) {
return accountInfoQueryService.findByAccountId(accountId)
.thenApply(AccountInfo::getTransactions);
public ResponseEntity<AccountHistoryResponse> getTransactionsHistory(@PathVariable String accountId) {
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
List<AccountHistoryEntry> historyEntries = new ArrayList<>();
historyEntries.add(new AccountOpenInfo(accountInfo.getDate(), AccountHistoryEntry.EntryType.account, accountInfo.getChanges().get(0).getAmount()));
accountInfo.getTransactions().forEach(historyEntries::add);
return ResponseEntity.ok().body(new AccountHistoryResponse(historyEntries));
}
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="account not found")

View File

@@ -1,5 +1,6 @@
apply plugin: 'java'
apply plugin: 'spring-boot'
apply plugin: EventuateDependencyPlugin
dependencies {
compile project(":common-auth-web")
@@ -14,7 +15,7 @@ dependencies {
}
task copyWebStatic(type: Copy) {
from "../../prebuilt-web-client"
from "../../js-frontend/build"
into "build/resources/main/static"
}

View File

@@ -4,6 +4,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.ApiGateway
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.ContentRequestTransformer;
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.HeadersRequestTransformer;
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.URLRequestTransformer;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
@@ -12,9 +13,12 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
@@ -26,6 +30,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@@ -52,13 +57,20 @@ public class GatewayController {
.build();
}
@RequestMapping(value = "/**", method = {GET, POST})
@RequestMapping(value = "/api/**", method = {GET, POST})
@ResponseBody
public ResponseEntity<String> proxyRequest(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, IOException, URISyntaxException {
HttpUriRequest proxiedRequest = createHttpUriRequest(request);
logger.info("request: {}", proxiedRequest);
HttpResponse proxiedResponse = httpClient.execute(proxiedRequest);
logger.info("Response {}", proxiedResponse.getStatusLine().getStatusCode());
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), processHeaders(proxiedResponse.getAllHeaders()), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
}
private HttpHeaders processHeaders(Header[] headers) {
HttpHeaders result = new HttpHeaders();
Stream.of(headers).filter(h -> h.getName().equalsIgnoreCase("Content-Type")).forEach( h -> result.set(h.getName(), h.getValue()));
return result;
}
private HttpUriRequest createHttpUriRequest(HttpServletRequest request) throws URISyntaxException, NoSuchRequestHandlingMethodException, IOException {

View File

@@ -5,18 +5,21 @@ customers.queryside.service.host=localhost
transfers.commandside.service.host=localhost
api.gateway.endpoints[0].path=[/]*accounts.*
api.gateway.endpoints[0].path=[/]*api/accounts.*
api.gateway.endpoints[0].method=GET
api.gateway.endpoints[0].location=http://${accounts.queryside.service.host}:8080
api.gateway.endpoints[1].path=[/]*accounts.*
api.gateway.endpoints[1].method=POST
api.gateway.endpoints[1].location=http://${accounts.commandside.service.host}:8080
api.gateway.endpoints[2].path=[/]*customers.*
api.gateway.endpoints[2].method=GET
api.gateway.endpoints[2].location=http://${customers.queryside.service.host}:8080
api.gateway.endpoints[3].path=[/]*customers.*
api.gateway.endpoints[3].method=POST
api.gateway.endpoints[3].location=http://${customers.commandside.service.host}:8080
api.gateway.endpoints[4].path=[/]*transfers.*
api.gateway.endpoints[1].path=[/]*api/customers.*/accounts
api.gateway.endpoints[1].method=GET
api.gateway.endpoints[1].location=http://${accounts.queryside.service.host}:8080
api.gateway.endpoints[2].path=[/]*api/accounts.*
api.gateway.endpoints[2].method=POST
api.gateway.endpoints[2].location=http://${accounts.commandside.service.host}:8080
api.gateway.endpoints[3].path=[/]*api/customers.*
api.gateway.endpoints[3].method=GET
api.gateway.endpoints[3].location=http://${customers.queryside.service.host}:8080
api.gateway.endpoints[4].path=[/]*api/customers.*
api.gateway.endpoints[4].method=POST
api.gateway.endpoints[4].location=http://${transfers.commandside.service.host}:8080
api.gateway.endpoints[4].location=http://${customers.commandside.service.host}:8080
api.gateway.endpoints[5].path=[/]*api/transfers.*
api.gateway.endpoints[5].method=POST
api.gateway.endpoints[5].location=http://${transfers.commandside.service.host}:8080

View File

@@ -3,6 +3,7 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;

View File

@@ -6,7 +6,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.a
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -6,7 +6,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.a
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
import org.junit.Assert;
import org.junit.Test;
@@ -17,6 +17,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import java.util.concurrent.CompletableFuture;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
@@ -55,10 +56,10 @@ public class AccountQuerySideIntegrationTest {
updatedTransaction -> Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.getEntity().getState()));
eventually(
() -> accountQueryService.findByAccountId(fromAccount.getEntityId()),
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(fromAccount.getEntityId())),
accountInfo -> Assert.assertEquals(70 * 100, accountInfo.getBalance()));
eventually(
() -> accountQueryService.findByAccountId(toAccount.getEntityId()),
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(toAccount.getEntityId())),
accountInfo -> Assert.assertEquals(380 * 100, accountInfo.getBalance()));
}
}

View File

@@ -0,0 +1,16 @@
import org.gradle.api.Plugin
import org.gradle.api.Project
class EventuateDependencyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.dependencies {
if (project.hasProperty("eventuateLocal")) {
compile "io.eventuate.local.java:eventuate-local-java-jdbc:${project.eventuateLocalVersion}"
compile "io.eventuate.local.java:eventuate-local-java-embedded-cdc-autoconfigure:${project.eventuateLocalVersion}"
} else
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:${project.eventuateClientVersion}"
}
}
}

View File

@@ -28,6 +28,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST;
*/
@RestController
@Validated
@RequestMapping("/api")
public class AuthController {
@Autowired

View File

@@ -80,7 +80,7 @@ public class AuthConfiguration extends WebSecurityConfigurerAdapter {
.authorizeRequests()
.antMatchers("/index.html", "/", "/**.js", "/**.css").permitAll()
.antMatchers("/swagger-ui.html", "/v2/api-docs").permitAll()
.antMatchers(HttpMethod.POST, "/customers", "/login").permitAll()
.antMatchers(HttpMethod.POST, "/api/customers", "/api/login").permitAll()
.anyRequest().authenticated().and()
.addFilterAfter(new StatelessAuthenticationFilter(tokenAuthenticationService), BasicAuthenticationFilter.class);
}

View File

@@ -16,9 +16,6 @@
<logger name="org.springframework" level='info'>
</logger>
<logger name="net.chrisrichardson.eventstore.javaexamples.banking" level='info'>
</logger>
<logger name="io.eventuate" level='debug'>
</logger>

View File

@@ -0,0 +1,80 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import java.util.Date;
public class AccountChangeInfo {
private String changeId;
private String transactionId;
private String transactionType;
private long amount;
private long balanceDelta;
public AccountChangeInfo() {
}
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
this(new Date(), changeId, transactionId, transactionType, amount, balanceDelta);
}
public AccountChangeInfo(Date date, String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
this.changeId = changeId;
this.transactionId = transactionId;
this.transactionType = transactionType;
this.amount = amount;
this.balanceDelta = balanceDelta;
}
public String getChangeId() {
return changeId;
}
public void setChangeId(String changeId) {
this.changeId = changeId;
}
public String getTransactionId() {
return transactionId;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
public String getTransactionType() {
return transactionType;
}
public void setTransactionType(String transactionType) {
this.transactionType = transactionType;
}
public long getAmount() {
return amount;
}
public void setAmount(long amount) {
this.amount = amount;
}
public long getBalanceDelta() {
return balanceDelta;
}
public void setBalanceDelta(long balanceDelta) {
this.balanceDelta = balanceDelta;
}
@Override
public boolean equals(Object o) {
return EqualsBuilder.reflectionEquals(this, o);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
}

View File

@@ -0,0 +1,50 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.Date;
/**
* Created by popikyardo on 9/1/16.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "entryType")
@JsonSubTypes({
@JsonSubTypes.Type(value = AccountTransactionInfo.class, name = "transaction"),
@JsonSubTypes.Type(value = AccountOpenInfo.class, name = "account")
})
public class AccountHistoryEntry {
protected Date date;
protected EntryType entryType;
public AccountHistoryEntry() {
}
public AccountHistoryEntry(Date date, EntryType entryType) {
this.date = date;
this.entryType = entryType;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public EntryType getEntryType() {
return entryType;
}
public void setEntryType(EntryType entryType) {
this.entryType = entryType;
}
public enum EntryType {
transaction, account
}
}

View File

@@ -0,0 +1,26 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
import java.util.List;
/**
* Created by popikyardo on 9/1/16.
*/
public class AccountHistoryResponse {
private List<AccountHistoryEntry> transactionsHistory;
public AccountHistoryResponse() {
}
public AccountHistoryResponse(List<AccountHistoryEntry> transactionsHistory) {
this.transactionsHistory = transactionsHistory;
}
public List<AccountHistoryEntry> getTransactionsHistory() {
return transactionsHistory;
}
public void setTransactionsHistory(List<AccountHistoryEntry> transactionsHistory) {
this.transactionsHistory = transactionsHistory;
}
}

View File

@@ -0,0 +1,27 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
import java.util.Date;
/**
* Created by popikyardo on 9/1/16.
*/
public class AccountOpenInfo extends AccountHistoryEntry {
private long initialBalance;
public AccountOpenInfo() {
}
public AccountOpenInfo(Date date, EntryType entryType, long initialBalance) {
super(date, entryType);
this.initialBalance=initialBalance;
}
public long getInitialBalance() {
return initialBalance;
}
public void setInitialBalance(long initialBalance) {
this.initialBalance = initialBalance;
}
}

View File

@@ -1,18 +1,20 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import java.util.Date;
public class AccountTransactionInfo {
public class AccountTransactionInfo extends AccountHistoryEntry{
private String transactionId;
private String fromAccountId;
private String toAccountId;
private long amount;
private Date date;
private String description;
private TransferState status = TransferState.INITIAL;
public AccountTransactionInfo() {
}
@@ -21,6 +23,12 @@ public class AccountTransactionInfo {
this(transactionId, fromAccountId, toAccountId, amount, new Date(), "");
}
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
public AccountTransactionInfo(String transactionId, String fromAccountId, String toAccountId, long amount, Date date, String description) {
this.transactionId = transactionId;
this.fromAccountId = fromAccountId;
@@ -28,6 +36,7 @@ public class AccountTransactionInfo {
this.amount = amount;
this.date = date;
this.description = description;
this.entryType = EntryType.transaction;
}
public String getTransactionId() {
@@ -62,14 +71,6 @@ public class AccountTransactionInfo {
this.amount = amount;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getDescription() {
return description;
}
@@ -78,6 +79,14 @@ public class AccountTransactionInfo {
this.description = description;
}
public TransferState getStatus() {
return status;
}
public void setStatus(TransferState status) {
this.status = status;
}
@Override
public boolean equals(Object o) {
return EqualsBuilder.reflectionEquals(this, o);

View File

@@ -0,0 +1,25 @@
package net.chrisrichardson.eventstore.javaexamples.banking.common.accounts;
import java.util.List;
/**
* Created by popikyardo on 9/1/16.
*/
public class GetAccountsResponse {
private List<GetAccountResponse> accounts;
public GetAccountsResponse() {
}
public GetAccountsResponse(List<GetAccountResponse> accounts) {
this.accounts = accounts;
}
public List<GetAccountResponse> getAccounts() {
return accounts;
}
public void setAccounts(List<GetAccountResponse> accounts) {
this.accounts = accounts;
}
}

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions;
package net.chrisrichardson.eventstore.javaexamples.banking.common.transactions;
public enum TransferState {
NEW, INITIAL, DEBITED, COMPLETED, FAILED_DUE_TO_INSUFFICIENT_FUNDS

View File

@@ -4,10 +4,12 @@ import io.eventuate.AggregateRepository;
import io.eventuate.EventuateAggregateStore;
import io.eventuate.javaclient.spring.EnableEventHandlers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableEventHandlers
@ComponentScan
public class CustomerConfiguration {
@Bean

View File

@@ -2,6 +2,7 @@ apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: 'spring-boot'
apply plugin: EventuateDependencyPlugin
dependencies {
compile project(":customers-command-side-web")
@@ -10,8 +11,7 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile project(":testutil")
testCompile "org.springframework.boot:spring-boot-starter-test"
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
}

View File

@@ -6,7 +6,6 @@ import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.custo
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.converter.HttpMessageConverter;
@@ -15,7 +14,6 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
@Configuration
@Import({CustomersCommandSideWebConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class CustomersCommandSideServiceConfiguration {

View File

@@ -25,7 +25,7 @@ public class CustomersCommandSideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Autowired

View File

@@ -1,6 +1,10 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.customers.CustomersCommandSideWebConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -13,9 +17,16 @@ import java.util.Arrays;
import java.util.List;
@Configuration
@Import({CustomersCommandSideServiceConfiguration.class, AuthConfiguration.class})
@Import({CustomersCommandSideWebConfiguration.class, EventuateJdbcEventStoreConfiguration.class, CommonSwaggerConfiguration.class, AuthConfiguration.class})
@EnableAutoConfiguration
public class CustomersCommandSideServiceTestConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new MappingJackson2HttpMessageConverter();
return new HttpMessageConverters(additional);
}
@Bean
public RestTemplate restTemplate(HttpMessageConverters converters) {
RestTemplate restTemplate = new RestTemplate();

View File

@@ -15,7 +15,7 @@ import java.util.concurrent.CompletableFuture;
* Created by popikyardo on 03.02.16.
*/
@RestController
@RequestMapping("/customers")
@RequestMapping("/api/customers")
public class CustomerController {
private CustomerService customerService;

View File

@@ -5,6 +5,7 @@ import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.Quer
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.ToAccountInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import java.util.Collections;
@@ -13,36 +14,38 @@ import java.util.Collections;
*/
public class CustomerInfoUpdateService {
private Logger logger = LoggerFactory.getLogger(getClass());
private Logger logger = LoggerFactory.getLogger(getClass());
private QuerySideCustomerRepository accountInfoRepository;
private QuerySideCustomerRepository querySideCustomerRepository;
public CustomerInfoUpdateService(QuerySideCustomerRepository accountInfoRepository) {
this.accountInfoRepository = accountInfoRepository;
}
public void create(String id, CustomerInfo customerInfo) {
try {
accountInfoRepository.save(new QuerySideCustomer(id,
customerInfo.getName(),
customerInfo.getEmail(),
customerInfo.getSsn(),
customerInfo.getPhoneNumber(),
customerInfo.getAddress(),
Collections.<String, ToAccountInfo>emptyMap()
)
);
logger.info("Saved in mongo");
} catch (Throwable t) {
logger.error("Error during saving: ", t);
throw new RuntimeException(t);
public CustomerInfoUpdateService(QuerySideCustomerRepository querySideCustomerRepository) {
this.querySideCustomerRepository = querySideCustomerRepository;
}
}
public void addToAccount(String id, ToAccountInfo accountInfo) {
QuerySideCustomer customer = accountInfoRepository.findOne(id);
customer.getToAccounts().put(accountInfo.getId(), accountInfo);
accountInfoRepository.save(customer);
}
public void create(String id, CustomerInfo customerInfo) {
try {
querySideCustomerRepository.save(new QuerySideCustomer(id,
customerInfo.getName(),
customerInfo.getEmail(),
customerInfo.getSsn(),
customerInfo.getPhoneNumber(),
customerInfo.getAddress(),
Collections.<String, ToAccountInfo>emptyMap()
)
);
logger.info("Saved in mongo");
} catch (DuplicateKeyException t) {
logger.warn("When saving ", t);
} catch (Throwable t) {
logger.error("Error during saving: ", t);
throw new RuntimeException(t);
}
}
public void addToAccount(String id, ToAccountInfo accountInfo) {
QuerySideCustomer customer = querySideCustomerRepository.findOne(id);
customer.getToAccounts().put(accountInfo.getId(), accountInfo);
querySideCustomerRepository.save(customer);
}
}

View File

@@ -2,6 +2,7 @@ package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.cu
import io.eventuate.javaclient.spring.EnableEventHandlers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@@ -12,6 +13,7 @@ import org.springframework.data.mongodb.repository.config.EnableMongoRepositorie
@Configuration
@EnableMongoRepositories
@EnableEventHandlers
@ComponentScan
public class QuerySideCustomerConfiguration {
@Bean
public CustomerQueryWorkflow customerQueryWorkflow(CustomerInfoUpdateService accountInfoUpdateService) {

View File

@@ -1,5 +1,6 @@
apply plugin: VerifyMongoDBConfigurationPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
apply plugin: EventuateDependencyPlugin
apply plugin: 'spring-boot'
@@ -10,8 +11,6 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile project(":testutil")
testCompile project(":customers-command-side-service")
testCompile "org.springframework.boot:spring-boot-starter-test"

View File

@@ -1,21 +1,19 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import io.eventuate.javaclient.spring.httpstomp.EventuateHttpStompClientConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.customers.QuerySideCustomerConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.CustomersQuerySideWebConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@Configuration
@Import({QuerySideCustomerConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
@Import({CustomersQuerySideWebConfiguration.class, EventuateHttpStompClientConfiguration.class, CommonSwaggerConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class CustomersQuerySideServiceConfiguration {

View File

@@ -1,8 +1,8 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.*;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerResponse;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.CustomersTestUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@@ -12,7 +12,6 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import javax.annotation.PostConstruct;
@@ -28,7 +27,7 @@ public class CustomersQuerySideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Autowired

View File

@@ -1,6 +1,7 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -13,7 +14,8 @@ import java.util.Arrays;
import java.util.List;
@Configuration
@Import({CustomersQuerySideServiceConfiguration.class, CustomersQuerySideServiceConfiguration.class, AuthConfiguration.class})
@Import({CustomersCommandSideServiceConfiguration.class, CustomersQuerySideServiceConfiguration.class, AuthConfiguration.class})
@EnableAutoConfiguration
public class CustomersQuerySideServiceTestConfiguration {
@Bean

View File

@@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture;
* Created by Main on 05.02.2016.
*/
@RestController
@RequestMapping("/api")
public class CustomerQueryController {
private CustomerQueryService customerQueryService;

View File

@@ -0,0 +1,129 @@
apigateway:
image: java:openjdk-8u91-jdk
working_dir: /app
volumes:
- ./api-gateway-service/build/libs:/app
command: java -jar /app/api-gateway-service.jar --accounts.commandside.service.host=accountscommandside --transfers.commandside.service.host=transactionscommandside --accounts.queryside.service.host=accountsqueryside --customers.commandside.service.host=customerscommandside --customers.queryside.service.host=customersqueryside
ports:
- "8080:8080"
links:
- accountscommandside
- transactionscommandside
- accountsqueryside
- customerscommandside
- customersqueryside
- mongodb
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
SPRING_DATA_MONGODB_URI: mongodb://mongodb/mydb
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
accountscommandside:
image: java:openjdk-8u91-jdk
working_dir: /app
volumes:
- ./accounts-command-side-service/build/libs:/app
command: java -jar /app/accounts-command-side-service.jar
ports:
- "8085:8080"
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
transactionscommandside:
image: java:openjdk-8u91-jdk
working_dir: /app
volumes:
- ./transactions-command-side-service/build/libs:/app
command: java -jar /app/transactions-command-side-service.jar
ports:
- "8082:8080"
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
accountsqueryside:
image: java:openjdk-8u91-jdk
working_dir: /app
volumes:
- ./accounts-query-side-service/build/libs:/app
command: java -jar /app/accounts-query-side-service.jar
ports:
- "8081:8080"
links:
- mongodb
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
SPRING_DATA_MONGODB_URI: mongodb://mongodb/mydb
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
customerscommandside:
image: java:openjdk-8u91-jdk
working_dir: /app
volumes:
- ./customers-command-side-service/build/libs:/app
command: java -jar /app/customers-command-side-service.jar
ports:
- "8083:8080"
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
customersqueryside:
image: java:openjdk-8u91-jdk
working_dir: /app
volumes:
- ./customers-query-side-service/build/libs:/app
command: java -jar /app/customers-query-side-service.jar
ports:
- "8084:8080"
links:
- mongodb
environment:
SPRING_DATASOURCE_URL:
SPRING_DATASOURCE_USERNAME:
SPRING_DATASOURCE_PASSWORD:
SPRING_DATASOURCE_DRIVER_CLASS_NAME:
EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS:
EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING:
SPRING_DATA_MONGODB_URI: mongodb://mongodb/mydb
EVENTUATELOCAL_EMBEDDED_CDC_DB_USER_NAME:
EVENTUATELOCAL_EMBEDDED_CDC_DB_PASSWORD:
mongodb:
image: mongo:3.0.4
hostname: mongodb
command: mongod --smallfiles
ports:
- "27017:27017"

View File

@@ -18,7 +18,7 @@ public class EndToEndTest extends AbstractRestAPITest {
CustomersTestUtils customersTestUtils = new CustomersTestUtils(restTemplate, baseUrl("/customers/"));
public String baseUrl(String path) {
return "http://" + getenv("SERVICE_HOST", "localhost") + ":" + 8080 + "/" + path;
return "http://" + getenv("SERVICE_HOST", "localhost") + ":" + 8080 + "/api" + path;
}
@Override

View File

@@ -6,4 +6,5 @@ eventuateMavenRepoUrl=http://mavenrepo.eventuate.io/release
springBootVersion=1.3.5.RELEASE
eventuateClientVersion=0.8.0.RELEASE
eventuateLocalVersion=0.2.0.RELEASE

View File

@@ -1,4 +1,3 @@
#! /bin/bash
docker run --link javaspring_mongodb_1:mongodb -i -t mongo:3.0.4 /usr/bin/mongo --host mongodb
docker run --rm --link javaspring_mongodb_1:mongodb -i -t mongo:3.0.4 /usr/bin/mongo --host mongodb

View File

@@ -15,6 +15,7 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
compile project(":common-swagger")
testCompile project(":testutil")
testCompile "org.springframework.boot:spring-boot-starter-test"
@@ -22,9 +23,9 @@ dependencies {
}
task copyWebStatic(type: Copy) {
from "../../prebuilt-web-client"
from "../../js-frontend/build"
into "build/resources/main/static"
}
jar.dependsOn(copyWebStatic)
bootRun.dependsOn(copyWebStatic)
bootRun.dependsOn(copyWebStatic)

View File

@@ -14,6 +14,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
@@ -31,6 +32,12 @@ public class BankingWebConfiguration extends WebMvcConfigurerAdapter {
return new HttpMessageConverters(additional);
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
System.setProperty("vertx.disableFileCPResolving", "true");
return new PropertySourcesPlaceholderConfigurer();
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("forward:/index.html");

View File

@@ -43,7 +43,7 @@ public class BankingAuthTest {
}
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Test
@@ -57,6 +57,12 @@ public class BankingAuthTest {
Assert.assertNotNull(customerId);
Assert.assertEquals(customerInfo, customerResponse.getCustomerInfo());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
customersTestUtils.assertCustomerResponse(customerId, customerInfo);
AuthRequest authRequest = new AuthRequest(email);

View File

@@ -1,6 +1,7 @@
package net.chrisrichardson.eventstore.javaexamples.banking.web;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.AbstractRestAPITest;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.AuthenticatedRestTemplate;
import net.chrisrichardson.eventstorestore.javaexamples.testutil.CustomersTestUtils;
import org.junit.runner.RunWith;
@@ -11,7 +12,6 @@ import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
@RunWith(SpringJUnit4ClassRunner.class)
@@ -32,7 +32,7 @@ public class BankingWebIntegrationTest extends AbstractRestAPITest {
@Override
public String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Override

7
java-spring/show-urls.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash -e
IP=$(docker-machine ip default)
echo Accounts command-side service = http://${IP}:8080/swagger-ui.html
echo Money Transfers command-side service = http://${IP}:8082/swagger-ui.html
echo Accounts query-side service = http://${IP}:8081/swagger-ui.html

View File

@@ -1,9 +1,6 @@
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountRequest;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.GetAccountResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.*;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerResponse;
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.QuerySideCustomer;
@@ -62,21 +59,21 @@ public abstract class AbstractRestAPITest {
assertAccountBalance(toAccountId, finalToAccountBalance);
eventually(
new Producer<AccountTransactionInfo[]>() {
new Producer<AccountHistoryResponse>() {
@Override
public CompletableFuture<AccountTransactionInfo[]> produce() {
public CompletableFuture<AccountHistoryResponse> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/accounts/" + fromAccountId + "/history"),
AccountTransactionInfo[].class));
AccountHistoryResponse.class));
}
},
new Verifier<AccountTransactionInfo[]>() {
new Verifier<AccountHistoryResponse>() {
@Override
public void verify(AccountTransactionInfo[] transactionInfos) {
Optional<AccountTransactionInfo> first = Arrays.asList(transactionInfos).stream().filter(ti -> ti.getTransactionId().equals(moneyTransfer.getMoneyTransferId())).findFirst();
public void verify(AccountHistoryResponse accountHistoryResponse) {
Optional<AccountHistoryEntry> first = accountHistoryResponse.getTransactionsHistory().stream().filter( ahe -> ahe.getEntryType() == AccountHistoryEntry.EntryType.transaction && ((AccountTransactionInfo)ahe).getTransactionId().equals(moneyTransfer.getMoneyTransferId())).findFirst();
assertTrue(first.isPresent());
AccountTransactionInfo ti = first.get();
AccountTransactionInfo ti = (AccountTransactionInfo)first.get();
assertEquals(fromAccountId, ti.getFromAccountId());
assertEquals(toAccountId, ti.getToAccountId());
@@ -111,17 +108,17 @@ public abstract class AbstractRestAPITest {
assertAccountBalance(accountId, initialFromAccountBalance);
eventually(
new Producer<GetAccountResponse[]>() {
new Producer<GetAccountsResponse>() {
@Override
public CompletableFuture<GetAccountResponse[]> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/accounts?customerId=" + customerId),
GetAccountResponse[].class));
public CompletableFuture<GetAccountsResponse> produce() {
return CompletableFuture.completedFuture(getAuthenticatedRestTemplate().getForEntity(baseUrl("/customers/"+customerId+"/accounts"),
GetAccountsResponse.class));
}
},
new Verifier<GetAccountResponse[]>() {
new Verifier<GetAccountsResponse>() {
@Override
public void verify(GetAccountResponse[] accountResponses) {
assertTrue(Arrays.asList(accountResponses).stream().filter(acc -> acc.getAccountId().equals(accountId)).findFirst().isPresent());
public void verify(GetAccountsResponse accountResponses) {
assertTrue(accountResponses.getAccounts().stream().filter(acc -> acc.getAccountId().equals(accountId)).findFirst().isPresent());
}
});
}

View File

@@ -1,6 +1,5 @@
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.utils.BasicAuthUtils;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;

View File

@@ -1,4 +1,4 @@
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.utils;
package net.chrisrichardson.eventstorestore.javaexamples.testutil;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.http.*;

View File

@@ -37,7 +37,7 @@ public class TestUtil {
}
static class Success<T> implements Outcome<T> {
static class Success<T> implements Outcome<T> {
T value;
@@ -54,51 +54,26 @@ public class TestUtil {
}
}
public static <T> void eventually(final Producer<T> producer, final Verifier<T> verifier) {
final int n = 50;
Object possibleException = Observable.timer(0, 200, TimeUnit.MILLISECONDS).flatMap(new Func1<Long, Observable<Outcome<T>>>() {
@Override
public Observable<Outcome<T>> call(Long aLong) {
try {
return fromCompletableFuture(producer.produce()).map(new Func1<T, Outcome<T>>() {
@Override
public Outcome<T> call(T t) {
return new Success<T>(t);
}
});
} catch (Exception e) {
Outcome<T> value = new Failure<T>(e);
return Observable.just(value);
}
public static <T> void eventually(Producer<T> producer, Verifier<T> predicate) {
Throwable laste = null;
for (int i = 0; i < 30 ; i++) {
try {
T x = producer.produce().get(30, TimeUnit.SECONDS);
predicate.verify(x);
return;
} catch (Throwable t) {
laste = t;
}
}).map(new Func1<Outcome<T>, Throwable>() {
@Override
public Throwable call(Outcome<T> t) {
try {
if (t instanceof Success) {
verifier.verify(((Success<T>) t).value);
return null;
} else
return ((Failure<T>) t).t;
} catch (Throwable e) {
return e;
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).take(n).zipWith(Observable.range(0, n), new Func2<Throwable, Integer, Tuple2<Throwable, Integer>>() {
@Override
public Tuple2<Throwable, Integer> call(Throwable e, Integer idx) {
return new Tuple2<Throwable, Integer>(e, idx);
}
}).skipWhile(new Func1<Tuple2<Throwable, Integer>, Boolean>() {
@Override
public Boolean call(Tuple2<Throwable, Integer> tuple2) {
return tuple2.first != null && tuple2.second < n - 1;
}
}).first().toBlocking().getIterator().next().first;
if (possibleException != null)
throw new RuntimeException((Throwable) possibleException);
}
if (laste != null)
throw new RuntimeException("Last exception was", laste);
else
throw new RuntimeException("predicate never satisfied");
}
private static <T> Observable<T> fromCompletableFuture(CompletableFuture<T> future) {

View File

@@ -4,6 +4,7 @@ import io.eventuate.Event;
import io.eventuate.EventUtil;
import io.eventuate.ReflectiveMutableCommandProcessingAggregate;
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.*;
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
import java.util.List;

View File

@@ -1,4 +1,5 @@
apply plugin: 'spring-boot'
apply plugin: EventuateDependencyPlugin
apply plugin: VerifyEventStoreEnvironmentPlugin
@@ -9,8 +10,6 @@ dependencies {
compile "org.springframework.boot:spring-boot-starter-web"
compile "org.springframework.boot:spring-boot-starter-actuator"
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:$eventuateClientVersion"
testCompile "org.springframework.boot:spring-boot-starter-test"
}

View File

@@ -20,7 +20,7 @@ public class TransactionsCommandSideServiceIntegrationTest {
private int port;
private String baseUrl(String path) {
return "http://localhost:" + port + "/" + path;
return "http://localhost:" + port + "/api" + path;
}
@Autowired

View File

@@ -16,7 +16,7 @@ import java.util.Date;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/transfers")
@RequestMapping("/api/transfers")
public class MoneyTransferController {
private final MoneyTransferService moneyTransferService;

View File

@@ -36,7 +36,7 @@ public class MoneyTransferControllerIntegrationTest {
@Test
public void shouldCreateAccount() throws Exception {
mockMvc.perform(post("/transfers")
mockMvc.perform(post("/api/transfers")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"fromAccountId\" : \"fromAccountId\", \"toAccountId\" : \"toAccountId\", \"amount\" : \"500\"}")
.accept(MediaType.APPLICATION_JSON))
@@ -45,7 +45,7 @@ public class MoneyTransferControllerIntegrationTest {
@Test
public void shouldRejectBadRequest() throws Exception {
mockMvc.perform(post("/transfers")
mockMvc.perform(post("/api/transfers")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"fromAccountId\" : \"fromAccountIdXXXXXX\"}, {\"toAccountId\" : \"toAccountId\"}, {\"amount\" : \"500\"}")
.accept(MediaType.APPLICATION_JSON))

View File

@@ -1,3 +1,3 @@
{
"presets": ["es2015"]
"presets": ["es2015", "stage-0", "react"]
}

View File

@@ -1,4 +1,5 @@
node_modules
build
!/build
#dist
dist-intermediate

View File

@@ -1,153 +1,15 @@
# Unicorn Standard Starter Kit
# Money Transfer App - Frontend Client
This starter kit provides you with the code and conventions you need to get straight into building your React/Redux based app.
This..
## Happiness is six lines away
*Prerequisites: node.js and git*
```
git clone https://github.com/unicorn-standard/starter-kit.git your-repo-name
cd your-repo-name
cd js-frontend
npm install
npm start
npm run open # (from a different console window, otherwise open localhost:3000)
npm run build
```
Presto, you've got a ready-to-customise application!
![Unicorn Standard Starter Kit](http://unicornstandard.com/files/boilerplate.png?1)
## Why use Unicorn Standard Starter Kit?
- Your directory structure is sorted as soon as you `git clone`
- ES6 compilation and automatic-reloading development server are configured for you with [webpack](https://webpack.github.io/) and [Babel](https://babeljs.io/)
- [Redux](http://redux.js.org/) is an incredibly simple way of modelling your data, with great community support
- [React](https://www.reactjs.org/) is an incredibly simple way of rendering your views, and is maintained by Facebook
- Simple [uniloc](http://unicornstandard.com/packages/uniloc.html)-based routing is included - easy to understand, and easy to customize
- The [Pacomo](http://unicornstandard.com/packages/pacomo.html) CSS conventions eliminate bugs caused by conflicting styles
- The actors pattern allows you to easily react to changes on your store *without* forcing a re-render
- Your redux store is already configured with navigation, data and view models
- Comes with views, layouts and reducers for a simple document editor!
## Getting Started
#### Put your name on it
- Update name, desription and author in `package.json`
- Update app title in `src/index.html`
- Restart the dev server (make sure to do this after any changes to `src/index.html`)
#### Make sure your editor is happy
- Setup ES6 syntax highlighting on extensions `.js` and `.jsx` (see [babel-sublime](https://github.com/babel/babel-sublime))
#### Start building
- Add a route to `src/constants/ROUTES.js`
- Add a nav menu item for your route in `src/components/ApplicationLayout.jsx`
- Add a component for your route in `src/components`
- Add reducers and actions for your component's view model in `src/actions` and `src/reducers/view`
- Add any data models which your component reqiures in `src/reducers/data`
- Add a container to map your store's `state` and `dispatch` to component props in `src/containers`
- Configure your route in `src/Application.jsx`
- Bask in the glory of your creation
- Don't forget to commit your changes and push to Bitbucket or GitHub!
#### Show your friends
- Run `gulp dist` to output a web-ready build of your app to `dist`
## Structure
### Entry point
`main.js` is the entry point to your application. It defines your redux store, handles any actions dispatched to your redux store, handles changes to the browser's current URL, and also makes an initial route change dispatch.
Most of the above will be obvious from a quick read through `main.js` - if there is one thing which may strike you as "interesting", it'll be the block which handles actors.
### Actors
*[Read the introduction to actors](http://jamesknelson.com/join-the-dark-side-of-the-flux-responding-to-actions-with-actors/)*
Each time your store's state changes, a sequence of functions are called on the *current state* of your store. These functions are called **actors**.
There is one important exception to this rule: actors will not be called if `main.js` is currently in the midst of calling the sequence from a previous update. This allows earlier actors in a sequence to dispatch actions to the store, with later actors in the sequence receiving the *updated* state.
The code which accomplishes this is very small:
```javascript
let acting = false
store.subscribe(function() {
// Ensure that any action dispatched by actors do not result in a new
// actor run, allowing actors to dispatch with impunity.
if (!acting) {
acting = true
for (let actor of actors) {
actor(store.getState(), store.dispatch.bind(store))
}
acting = false
}
})
```
The actor is defined in `src/actors/index.js`. By default, it runs the following sequence:
- **redirector** - dispatch a navigation action if the current location should redirect to another location
- **renderer** - renders your <Application> component with React
### Model
Your model (i.e. reducers and actions) is pre-configured with three parts:
#### Navigation
The `navigation` state holds the following information:
- `location` is the object which your `ROUTES` constant's `lookup` function returns for the current URL. With the default uniloc-based `ROUTES` object, this will have a string `name` property, and an `options` object containing any route parameters.
- `transitioning` is true if a navigation `start` action has been dispatched, but the browser hasn't changed URL yet
#### Data
The `data` state can be thought of as the database for your application. If your application reads data from a remote server, it should be stored here. Any metadata should also be stored here, including the time it was fetched or its current version number.
#### View
The `view` state has a property for each of the view's in your app, holding their current state. For example, form state should be stored in the view models.
### Directories
- `src/actions` - Redux action creators
- `src/actors` - Handle changes to your store's state
- `src/components` - React components, stateless where possible
- `src/constants` - Define stateless data
- `src/containers` - Unstyled "smart" components which take your store's `state` and `dispatch`, and possibly navigation `location`, and pass them to "dumb" components
- `src/reducers` - Redux reducers
- `src/static` - Files which will be copied across to the root directory on build
- `src/styles` - Helpers for stylesheets for individual components
- `src/utils` - General code which isn't specific to your application
- `src/validators` - Functions which take an object containing user entry and return an object containing any errors
Other directories:
- `build` - Intermediate files produced by the development server. Don't touch these.
- `dist` - The output of `gulp dist`, which contains your distribution-ready app.
- `config/environments` - The build system will assign one of these to the `environment` module, depending on the current build environment.
Main application files:
- `src/Application.jsx` - Your application's top-level React component
- `src/index.html` - The single page for your single page application
- `src/main.js` - The application's entry point
- `src/main.less` - Global styles for your application
Main build files:
- `gulpfile.babel.js` - Build scripts written with [gulp](http://gulpjs.com/)
- `webpack.config.js` - [Webpack](http://webpack.github.io/) configuration
## TODO
- Watch `static` and `index.html` for changes and copy them across to `build` when appropriate
Text..

View File

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<!--[if lt IE 7 ]> <html lang="en" class="ie6" > <![endif]-->
<!--[if IE 7 ]> <html lang="en" class="ie7" > <![endif]-->
<!--[if IE 8 ]> <html lang="en" class="ie8" > <![endif]-->
<!--[if IE 9 ]> <html lang="en" class="ie9" > <![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="" > <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Money Transfer App</title><meta name="description" content="ES Money Transfer App" /><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /><!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css"><link href="/style.6d7a32b1405ea1bb2bdf.css" rel="stylesheet"></head>
<body><div id="root"></div><script src="/manifest.09cb8f5a05c9cfc35585.js"></script><script src="/vendor.f73c0104cb72cfb2809e.js"></script><script src="/style.6d7a32b1405ea1bb2bdf.js"></script><script src="/app.d4bdff82ac1db214898b.js"></script><script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXX-XX', 'auto');
ga('send', 'pageview');
</script><!--{"files":{"publicPath":"/","chunks":{"manifest":{"size":0,"entry":"/manifest.09cb8f5a05c9cfc35585.js","hash":"09cb8f5a05c9cfc35585","css":[]},"vendor":{"size":1670874,"entry":"/vendor.f73c0104cb72cfb2809e.js","hash":"f73c0104cb72cfb2809e","css":[]},"style":{"size":122,"entry":"/style.6d7a32b1405ea1bb2bdf.js","hash":"6d7a32b1405ea1bb2bdf","css":["/style.6d7a32b1405ea1bb2bdf.css"]},"app":{"size":351315,"entry":"/app.d4bdff82ac1db214898b.js","hash":"d4bdff82ac1db214898b","css":[]}},"js":["/manifest.09cb8f5a05c9cfc35585.js","/vendor.f73c0104cb72cfb2809e.js","/style.6d7a32b1405ea1bb2bdf.js","/app.d4bdff82ac1db214898b.js"],"css":["/style.6d7a32b1405ea1bb2bdf.css"]},"options":{"template":"/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/node_modules/html-webpack-plugin/lib/loader.js!/Users/andrew/dev/clients/ES/code/event-sourcing-examples/js-frontend/public/index.ejs","filename":"index.html","hash":false,"inject":false,"compile":true,"favicon":false,"minify":false,"cache":true,"showErrors":true,"chunks":"all","excludeChunks":[],"title":"Money Transfer App","xhtml":false,"description":"ES Money Transfer App","appMountId":"root","googleAnalytics":{"trackingId":"UA-XXXX-XX","pageViewOnLoad":true},"mobile":true}}--></body>
</html>

View File

@@ -0,0 +1,95 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ var parentJsonpFunction = window["webpackJsonp"];
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, callbacks = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId])
/******/ callbacks.push.apply(callbacks, installedChunks[chunkId]);
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
/******/ while(callbacks.length)
/******/ callbacks.shift().call(null, __webpack_require__);
/******/ if(moreModules[0]) {
/******/ installedModules[0] = 0;
/******/ return __webpack_require__(0);
/******/ }
/******/ };
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // object to store loaded and loading chunks
/******/ // "0" means "already loaded"
/******/ // Array means "loading", array contains callbacks
/******/ var installedChunks = {
/******/ 3:0
/******/ };
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId, callback) {
/******/ // "0" is the signal for "already loaded"
/******/ if(installedChunks[chunkId] === 0)
/******/ return callback.call(null, __webpack_require__);
/******/
/******/ // an array means "currently loading".
/******/ if(installedChunks[chunkId] !== undefined) {
/******/ installedChunks[chunkId].push(callback);
/******/ } else {
/******/ // start chunk loading
/******/ installedChunks[chunkId] = [callback];
/******/ var head = document.getElementsByTagName('head')[0];
/******/ var script = document.createElement('script');
/******/ script.type = 'text/javascript';
/******/ script.charset = 'utf-8';
/******/ script.async = true;
/******/
/******/ script.src = __webpack_require__.p + "" + {"0":"d4bdff82ac1db214898b","1":"6d7a32b1405ea1bb2bdf","2":"f73c0104cb72cfb2809e"}[chunkId] + ".js";
/******/ head.appendChild(script);
/******/ }
/******/ };
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/";
/******/ })
/************************************************************************/
/******/ ([]);
//# sourceMappingURL=manifest.09cb8f5a05c9cfc35585.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["webpack:///webpack/bootstrap 8ac05bd1ead33e72514a?"],"names":[],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAQ,oBAAoB;AAC5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,YAAI;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,uDAA+C,iFAAiF;AAChI;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA","file":"manifest.09cb8f5a05c9cfc35585.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, callbacks = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId])\n \t\t\t\tcallbacks.push.apply(callbacks, installedChunks[chunkId]);\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);\n \t\twhile(callbacks.length)\n \t\t\tcallbacks.shift().call(null, __webpack_require__);\n \t\tif(moreModules[0]) {\n \t\t\tinstalledModules[0] = 0;\n \t\t\treturn __webpack_require__(0);\n \t\t}\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// \"0\" means \"already loaded\"\n \t// Array means \"loading\", array contains callbacks\n \tvar installedChunks = {\n \t\t3:0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId, callback) {\n \t\t// \"0\" is the signal for \"already loaded\"\n \t\tif(installedChunks[chunkId] === 0)\n \t\t\treturn callback.call(null, __webpack_require__);\n\n \t\t// an array means \"currently loading\".\n \t\tif(installedChunks[chunkId] !== undefined) {\n \t\t\tinstalledChunks[chunkId].push(callback);\n \t\t} else {\n \t\t\t// start chunk loading\n \t\t\tinstalledChunks[chunkId] = [callback];\n \t\t\tvar head = document.getElementsByTagName('head')[0];\n \t\t\tvar script = document.createElement('script');\n \t\t\tscript.type = 'text/javascript';\n \t\t\tscript.charset = 'utf-8';\n \t\t\tscript.async = true;\n\n \t\t\tscript.src = __webpack_require__.p + \"\" + {\"0\":\"d4bdff82ac1db214898b\",\"1\":\"6d7a32b1405ea1bb2bdf\",\"2\":\"f73c0104cb72cfb2809e\"}[chunkId] + \".js\";\n \t\t\thead.appendChild(script);\n \t\t}\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 8ac05bd1ead33e72514a\n **/"],"sourceRoot":""}

View File

@@ -0,0 +1,5 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:

View File

@@ -0,0 +1,441 @@
@import url(http://fonts.googleapis.com/css?family=Roboto:300,400,500);
/**
* React Select
* ============
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
* MIT License: https://github.com/keystonejs/react-select
*/
.Select {
position: relative;
}
.Select,.Select div,.Select input,.Select span {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.Select.is-disabled > .Select-control {
background-color: #f6f6f6;
}
.Select.is-disabled .Select-arrow-zone {
cursor: default;
pointer-events: none;
}
.Select-control {
background-color: #fff;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
color: #333;
cursor: default;
display: table;
height: 36px;
outline: none;
overflow: hidden;
position: relative;
width: 100%;
}
.Select-control:hover {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}
.is-searchable.is-open > .Select-control {
cursor: text;
}
.is-open > .Select-control {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
background: #fff;
border-color: #b3b3b3 #ccc #d9d9d9;
}
.is-open > .Select-control > .Select-arrow {
border-color: transparent transparent #999;
border-width: 0 5px 5px;
}
.is-searchable.is-focused:not(.is-open) > .Select-control {
cursor: text;
}
.is-focused:not(.is-open) > .Select-control {
border-color: #08c #0099e6 #0099e6;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
}
.Select-placeholder {
bottom: 0;
color: #aaa;
left: 0;
line-height: 34px;
padding-left: 10px;
padding-right: 10px;
position: absolute;
right: 0;
top: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.has-value > .Select-control > .Select-placeholder {
color: #333;
}
.Select-value {
color: #aaa;
left: 0;
padding: 8px 52px 8px 10px;
position: absolute;
right: -15px;
top: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.has-value > .Select-control > .Select-value {
color: #333;
}
.Select-input {
height: 34px;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
}
.Select-input > input {
background: none transparent;
border: 0 none;
box-shadow: none;
cursor: default;
display: inline-block;
font-family: inherit;
font-size: inherit;
height: 34px;
margin: 0;
outline: none;
padding: 0;
-webkit-appearance: none;
}
.is-focused .Select-input > input {
cursor: text;
}
.Select-control:not(.is-searchable) > .Select-input {
outline: none;
}
.Select-loading-zone {
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 16px;
}
.Select-loading {
-webkit-animation: Select-animation-spin 400ms infinite linear;
-o-animation: Select-animation-spin 400ms infinite linear;
animation: Select-animation-spin 400ms infinite linear;
width: 16px;
height: 16px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid #ccc;
border-right-color: #333;
display: inline-block;
position: relative;
vertical-align: middle;
}
.Select-clear-zone {
-webkit-animation: Select-animation-fadeIn 200ms;
-o-animation: Select-animation-fadeIn 200ms;
animation: Select-animation-fadeIn 200ms;
color: #999;
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 17px;
}
.Select-clear-zone:hover {
color: #D0021B;
}
.Select-clear {
display: inline-block;
font-size: 18px;
line-height: 1;
}
.Select--multi .Select-clear-zone {
width: 17px;
}
.Select-arrow-zone {
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 25px;
padding-right: 5px;
}
.Select-arrow {
border-color: #999 transparent transparent;
border-style: solid;
border-width: 5px 5px 2.5px;
display: inline-block;
height: 0;
width: 0;
}
.is-open .Select-arrow,.Select-arrow-zone:hover > .Select-arrow {
border-top-color: #666;
}
@-webkit-keyframes Select-animation-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes Select-animation-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.Select-menu-outer {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
border: 1px solid #ccc;
border-top-color: #e6e6e6;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
box-sizing: border-box;
margin-top: -1px;
max-height: 200px;
position: absolute;
top: 100%;
width: 100%;
z-index: 1000;
-webkit-overflow-scrolling: touch;
}
.Select-menu {
max-height: 198px;
overflow-y: auto;
}
.Select-option {
box-sizing: border-box;
color: #666666;
cursor: pointer;
display: block;
padding: 8px 10px;
}
.Select-option:last-child {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.Select-option.is-focused {
background-color: #f2f9fc;
color: #333;
}
.Select-option.is-disabled {
color: #cccccc;
cursor: not-allowed;
}
.Select-noresults,.Select-search-prompt,.Select-searching {
box-sizing: border-box;
color: #999999;
cursor: default;
display: block;
padding: 8px 10px;
}
.Select--multi .Select-input {
vertical-align: middle;
margin-left: 10px;
padding: 0;
}
.Select--multi.has-value .Select-input {
margin-left: 5px;
}
.Select-item {
background-color: #f2f9fc;
border-radius: 2px;
border: 1px solid #c9e6f2;
color: #08c;
display: inline-block;
font-size: 0.9em;
margin-left: 5px;
margin-top: 5px;
vertical-align: top;
}
.Select-item-icon,.Select-item-label {
display: inline-block;
vertical-align: middle;
}
.Select-item-label {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
cursor: default;
padding: 2px 5px;
}
.Select-item-label .Select-item-label__a {
color: #08c;
cursor: pointer;
}
.Select-item-icon {
cursor: pointer;
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
border-right: 1px solid #c9e6f2;
padding: 1px 5px 3px;
}
.Select-item-icon:hover,.Select-item-icon:focus {
background-color: #ddeff7;
color: #0077b3;
}
.Select-item-icon:active {
background-color: #c9e6f2;
}
.Select--multi.is-disabled .Select-item {
background-color: #f2f2f2;
border: 1px solid #d9d9d9;
color: #888;
}
.Select--multi.is-disabled .Select-item-icon {
cursor: not-allowed;
border-right: 1px solid #d9d9d9;
}
.Select--multi.is-disabled .Select-item-icon:hover,.Select--multi.is-disabled .Select-item-icon:focus,.Select--multi.is-disabled .Select-item-icon:active {
background-color: #f2f2f2;
}
@keyframes Select-animation-spin {
to {
transform: rotate(1turn);
}
}
@-webkit-keyframes Select-animation-spin {
to {
-webkit-transform: rotate(1turn);
}
}
/*
* This file contains Global styles.
*
* In general, your styles should *not* be in this file, but in the individual
* component files. For details, see the Pacomo specification:
*
* https://github.com/unicorn-standard/pacomo
*/
* {
box-sizing: border-box;
margin: 0;
}
*:before,*:after {
box-sizing: border-box;
}
html,body,main {
position: relative;
height: 100%;
min-height: 100%;
font-family: Roboto;
}
body {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
input,button,select,textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
#react-app {
position: relative;
height: 100%;
min-height: 100%;
}
body {
padding-bottom: 50px;
/* height: 100%; */
/* min-height: 100%; */
height: auto;
}
.footer-navigation {
height: 1px;
}
.footer-navigation > .container {
height: 100%;
}
.footer-navigation > .container > * {
top: 50%;
transform: translateY(-50%);
}
.page-header {
padding-bottom: 9px;
margin: 0px 0 20px;
border-bottom: 1px solid #eee;
}
h1 {
margin-top: .5em;
}
/*# sourceMappingURL=style.6d7a32b1405ea1bb2bdf.css.map*/

View File

@@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"style.6d7a32b1405ea1bb2bdf.css","sourceRoot":""}

View File

@@ -0,0 +1,27 @@
webpackJsonp([1,3],{
/***/ 0:
/***/ function(module, exports, __webpack_require__) {
__webpack_require__(614);
module.exports = __webpack_require__(618);
/***/ },
/***/ 614:
/***/ function(module, exports) {
// removed by extract-text-webpack-plugin
/***/ },
/***/ 618:
/***/ function(module, exports) {
// removed by extract-text-webpack-plugin
/***/ }
});
//# sourceMappingURL=style.6d7a32b1405ea1bb2bdf.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./~/react-select/dist/react-select.css?","webpack:///./src/main.less?"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,0C;;;;;;;ACAA,0C","file":"style.6d7a32b1405ea1bb2bdf.js","sourcesContent":["// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./~/react-select/dist/react-select.css\n ** module id = 614\n ** module chunks = 1\n **/","// removed by extract-text-webpack-plugin\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./src/main.less\n ** module id = 618\n ** module chunks = 1\n **/"],"sourceRoot":""}

View File

@@ -0,0 +1,334 @@
/**
* React Select
* ============
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
* MIT License: https://github.com/keystonejs/react-select
*/
.Select {
position: relative;
}
.Select,
.Select div,
.Select input,
.Select span {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.Select.is-disabled > .Select-control {
background-color: #f9f9f9;
}
.Select.is-disabled > .Select-control:hover {
box-shadow: none;
}
.Select.is-disabled .Select-arrow-zone {
cursor: default;
pointer-events: none;
}
.Select-control {
background-color: #fff;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
color: #333;
cursor: default;
display: table;
height: 36px;
outline: none;
overflow: hidden;
position: relative;
width: 100%;
}
.Select-control:hover {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}
.is-searchable.is-open > .Select-control {
cursor: text;
}
.is-open > .Select-control {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
background: #fff;
border-color: #b3b3b3 #ccc #d9d9d9;
}
.is-open > .Select-control > .Select-arrow {
border-color: transparent transparent #999;
border-width: 0 5px 5px;
}
.is-searchable.is-focused:not(.is-open) > .Select-control {
cursor: text;
}
.is-focused:not(.is-open) > .Select-control {
border-color: #007eff;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1);
}
.Select-placeholder,
:not(.Select--multi) > .Select-control .Select-value {
bottom: 0;
color: #aaa;
left: 0;
line-height: 34px;
padding-left: 10px;
padding-right: 10px;
position: absolute;
right: 0;
top: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label {
color: #333;
}
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label {
cursor: pointer;
text-decoration: none;
}
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus {
color: #007eff;
outline: none;
text-decoration: underline;
}
.Select-input {
height: 34px;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
}
.Select-input > input {
background: none transparent;
border: 0 none;
box-shadow: none;
cursor: default;
display: inline-block;
font-family: inherit;
font-size: inherit;
height: 34px;
margin: 0;
outline: none;
padding: 0;
-webkit-appearance: none;
}
.is-focused .Select-input > input {
cursor: text;
}
.has-value.is-pseudo-focused .Select-input {
opacity: 0;
}
.Select-control:not(.is-searchable) > .Select-input {
outline: none;
}
.Select-loading-zone {
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 16px;
}
.Select-loading {
-webkit-animation: Select-animation-spin 400ms infinite linear;
-o-animation: Select-animation-spin 400ms infinite linear;
animation: Select-animation-spin 400ms infinite linear;
width: 16px;
height: 16px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid #ccc;
border-right-color: #333;
display: inline-block;
position: relative;
vertical-align: middle;
}
.Select-clear-zone {
-webkit-animation: Select-animation-fadeIn 200ms;
-o-animation: Select-animation-fadeIn 200ms;
animation: Select-animation-fadeIn 200ms;
color: #999;
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 17px;
}
.Select-clear-zone:hover {
color: #D0021B;
}
.Select-clear {
display: inline-block;
font-size: 18px;
line-height: 1;
}
.Select--multi .Select-clear-zone {
width: 17px;
}
.Select-arrow-zone {
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 25px;
padding-right: 5px;
}
.Select-arrow {
border-color: #999 transparent transparent;
border-style: solid;
border-width: 5px 5px 2.5px;
display: inline-block;
height: 0;
width: 0;
}
.is-open .Select-arrow,
.Select-arrow-zone:hover > .Select-arrow {
border-top-color: #666;
}
@-webkit-keyframes Select-animation-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes Select-animation-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.Select-menu-outer {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
border: 1px solid #ccc;
border-top-color: #e6e6e6;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
box-sizing: border-box;
margin-top: -1px;
max-height: 200px;
position: absolute;
top: 100%;
width: 100%;
z-index: 1;
-webkit-overflow-scrolling: touch;
}
.Select-menu {
max-height: 198px;
overflow-y: auto;
}
.Select-option {
box-sizing: border-box;
background-color: #fff;
color: #666666;
cursor: pointer;
display: block;
padding: 8px 10px;
}
.Select-option:last-child {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.Select-option.is-focused {
background-color: rgba(0, 126, 255, 0.08);
color: #333;
}
.Select-option.is-disabled {
color: #cccccc;
cursor: default;
}
.Select-noresults {
box-sizing: border-box;
color: #999999;
cursor: default;
display: block;
padding: 8px 10px;
}
.Select--multi .Select-input {
vertical-align: middle;
margin-left: 10px;
padding: 0;
}
.Select--multi.has-value .Select-input {
margin-left: 5px;
}
.Select--multi .Select-value {
background-color: rgba(0, 126, 255, 0.08);
border-radius: 2px;
border: 1px solid rgba(0, 126, 255, 0.24);
color: #007eff;
display: inline-block;
font-size: 0.9em;
line-height: 1.4;
margin-left: 5px;
margin-top: 5px;
vertical-align: top;
}
.Select--multi .Select-value-icon,
.Select--multi .Select-value-label {
display: inline-block;
vertical-align: middle;
}
.Select--multi .Select-value-label {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
cursor: default;
padding: 2px 5px;
}
.Select--multi a.Select-value-label {
color: #007eff;
cursor: pointer;
text-decoration: none;
}
.Select--multi a.Select-value-label:hover {
text-decoration: underline;
}
.Select--multi .Select-value-icon {
cursor: pointer;
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
border-right: 1px solid rgba(0, 126, 255, 0.24);
padding: 1px 5px 3px;
}
.Select--multi .Select-value-icon:hover,
.Select--multi .Select-value-icon:focus {
background-color: rgba(0, 113, 230, 0.08);
color: #0071e6;
}
.Select--multi .Select-value-icon:active {
background-color: rgba(0, 126, 255, 0.24);
}
.Select--multi.is-disabled .Select-value {
background-color: #fcfcfc;
border: 1px solid #e3e3e3;
color: #333;
}
.Select--multi.is-disabled .Select-value-icon {
cursor: not-allowed;
border-right: 1px solid #e3e3e3;
}
.Select--multi.is-disabled .Select-value-icon:hover,
.Select--multi.is-disabled .Select-value-icon:focus,
.Select--multi.is-disabled .Select-value-icon:active {
background-color: #fcfcfc;
}
@keyframes Select-animation-spin {
to {
transform: rotate(1turn);
}
}
@-webkit-keyframes Select-animation-spin {
to {
-webkit-transform: rotate(1turn);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,217 @@
/**
* Created by andrew on 8/18/16.
*/
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PurifyCSSPlugin = require('purifycss-webpack-plugin');
exports.devServer = function(options) {
return {
entry: {
'webpack-dev-server': 'webpack-dev-server/client?http://localhost:8080',
hmr: 'webpack/hot/only-dev-server'
},
devServer: {
contentBase: './build',
hot: true,
historyApiFallback: true,
inline: true,
stats: 'errors-only',
host: options.host, // Defaults to `localhost`
port: options.port, // Defaults to 8080
proxy: {
'/api*' : {
target: 'http://localhost:8080'
}
// '/user*' : {
// target: 'http://localhost:8080'
// },
// '/login' : {
// target: 'http://localhost:8080'
// },
// '/customers*' : {
// target: 'http://localhost:8080'
// },
// '/accounts*' : {
// target: 'http://localhost:8080'
// },
// '/transfers*' : {
// target: 'http://localhost:8080'
// }
}
},
watchOptions: {
// Delay the rebuild after the first change
aggregateTimeout: 300,
// Poll using interval (in ms, accepts boolean too)
poll: 1000
},
plugins: [
// Enable multi-pass compilation for enhanced performance
// in larger projects. Good default.
new webpack.HotModuleReplacementPlugin({
multiStep: true
})
]
};
};
exports.setupCSS = function(paths) {
return {
module: {
loaders: [
{
test: /\.css$/,
loaders: ['style', 'css'],
include: paths
}
]
}
};
};
exports.setupLess = function(paths) {
return {
module: {
loaders: [
{
test: /\.(le)|(c)ss$/,
loaders: ['style', 'css', 'less'],
include: paths
}
]
}
};
};
exports.useJSON = function() {
return {
module: {
loaders: [
{
test: /\.json$/,
loaders: ['json']
}
]
}
};
};
exports.minify = function() {
return {
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
]
};
};
exports.extractBundle = function(options) {
const entry = {};
entry[options.name] = options.entries;
return {
// Define an entry point needed for splitting.
entry: entry,
plugins: [
// Extract bundle and manifest files. Manifest is
// needed for reliable caching.
new webpack.optimize.CommonsChunkPlugin({
names: [options.name, 'manifest']
})
]
};
};
exports.clean = function(path) {
return {
plugins: [
new CleanWebpackPlugin([path], {
// Without `root` CleanWebpackPlugin won't point to our
// project and will fail to work.
root: process.cwd()
})
]
};
};
exports.extractCSS = function(paths) {
return {
module: {
loaders: [
// Extract CSS during build
{
test: /\.css$/,
// loaders: ['style', 'css'],
loader: ExtractTextPlugin.extract('style', 'css'),
include: paths
}
]
},
plugins: [
// Output extracted CSS to a file
new ExtractTextPlugin('[name].[chunkhash].css')
]
};
};
exports.extractLESS = function(paths) {
return {
module: {
loaders: [
// Extract CSS during build
{
test: /\.(le)|(c)ss$/,
// loaders: ['style', 'css', 'less'],
loader: ExtractTextPlugin.extract(
"style-loader",
'css?sourceMap!' +
'less?sourceMap'
),
include: paths
}
]
},
plugins: [
// Output extracted CSS to a file
new ExtractTextPlugin('[name].[chunkhash].css')
]
};
};
exports.purifyCSS = function(paths) {
return {
plugins: [
new PurifyCSSPlugin({
basePath: process.cwd(),
// `paths` is used to point PurifyCSS to files not
// visible to Webpack. You can pass glob patterns
// to it.
paths: paths,
purifyOptions: {
// minify: false,
// info: true,
// output: './output.css'
}
})
]
}
};
exports.useJQuery = function() {
return {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
})
]
};
};

View File

@@ -1,7 +1,7 @@
{
"name": "",
"description": "",
"author": "",
"name": "event-sourcing-examples",
"description": "ES Money Transfer App",
"author": "cer",
"private": true,
"version": "0.1.0",
"license": "MIT",
@@ -9,20 +9,26 @@
"scripts": {
"start": "gulp serve",
"open": "gulp open",
"gulp": "gulp"
"gulp": "gulp",
"build": "webpack",
"watch": "webpack --progress --colors --watch --display-error-details --display-chunks --profile",
"start-dev": "export PORT=3000 && webpack-dev-server --host 0.0.0.0"
},
"devDependencies": {
"autoprefixer-loader": "^2.0.0",
"babel-core": "6.1.4",
"babel-loader": "6.1.0",
"babel-plugin-add-module-exports": "^0.1.2",
"babel-plugin-transform-runtime": "6.1.4",
"babel-polyfill": "^6.1.4",
"babel-preset-es2015": "6.1.4",
"babel-preset-react": "6.1.4",
"babel-preset-stage-0": "6.1.2",
"babel-register": "6.1.4",
"babel-runtime": "^6.0.14",
"babel-plugin-transform-runtime": "^6.12.0",
"babel-cli": "^6.7.7",
"babel-core": "^6.10.4",
"babel-eslint": "^4.1.6",
"babel-loader": "^6.2.4",
"babel-polyfill": "^6.2.0",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.1.2",
"clean-webpack-plugin": "^0.1.10",
"copy-webpack-plugin": "^3.0.1",
"css-loader": "^0.14.4",
"del": "^1.2.0",
"extract-text-webpack-plugin": "^0.8.1",
@@ -35,17 +41,22 @@
"gulp-load-plugins": "^0.10.0",
"gulp-size": "^1.2.1",
"gulp-util": "^3.0.5",
"html-webpack-plugin": "^2.22.0",
"json-loader": "^0.5.4",
"less": "^2.5.3",
"less-loader": "^2.2.0",
"node-libs-browser": "^0.5.2",
"open": "0.0.5",
"purifycss-webpack-plugin": "^2.0.3",
"react-hot-loader": "^1.3.0",
"redux-devtools": "^2.1.5",
"run-sequence": "^1.1.0",
"style-loader": "^0.12.3",
"url-loader": "^0.5.6",
"webpack": "^1.9.10",
"webpack-dev-server": "^1.9.0"
"webpack-dev-server": "^1.9.0",
"webpack-merge": "^0.14.1",
"webpack-validator": "^2.2.7"
},
"dependencies": {
"classnames": "^2.2.3",
@@ -59,10 +70,10 @@
"react": "^0.14.7",
"react-bootstrap": "^0.28.3",
"react-dom": "^0.14.0",
"react-loader": "^2.0.0",
"react-loader": "^2.4.0",
"react-pacomo": "^0.5.1",
"react-redux": "^4.4.0",
"react-router": "^2.0.0-rc2",
"react-router": "^2.7.0",
"react-router-bootstrap": "^0.20.1",
"react-router-redux": "^3.0.0",
"react-select": "^0.9.1",

View File

View File

@@ -0,0 +1,81 @@
<!DOCTYPE html>
<!--[if lt IE 7 ]> <html lang="en" class="ie6" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
<!--[if IE 7 ]> <html lang="en" class="ie7" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
<!--[if IE 8 ]> <html lang="en" class="ie8" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
<!--[if IE 9 ]> <html lang="en" class="ie9" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="" <% if(htmlWebpackPlugin.files.manifest) { %> manifest="<%= htmlWebpackPlugin.files.manifest %>"<% } %>> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title><%= htmlWebpackPlugin.options.title || 'Webpack App'%></title><%
if (htmlWebpackPlugin.options.description) {
%><meta name="description" content="<%= htmlWebpackPlugin.options.description%>" /><%
} %><%
if (htmlWebpackPlugin.files.favicon) {
%><link rel="shortcut icon" href="<%= htmlWebpackPlugin.files.favicon%>"><%
} %><%
if (htmlWebpackPlugin.options.mobile) {
%><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" /><%
} %><!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min.css"><%
for (var css in htmlWebpackPlugin.files.css) {
%><link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet"><%
} %><%
if (htmlWebpackPlugin.options.baseHref) {
%><base href="<%= htmlWebpackPlugin.options.baseHref %>" /><%
} %></head>
<body><%
if (htmlWebpackPlugin.options.unsupportedBrowser) {
%><style>.unsupported-browser { display: none; }</style>
<div class="unsupported-browser">
Sorry, your browser is not supported. Please upgrade to
the latest version or switch your browser to use this site.
See <a href="http://outdatedbrowser.com/">outdatedbrowser.com</a>
for options.
</div><%
} %><%
if (htmlWebpackPlugin.options.appMountId) {
%><div id="<%= htmlWebpackPlugin.options.appMountId%>"></div><%
} %><%
if (htmlWebpackPlugin.options.appMountIds && htmlWebpackPlugin.options.appMountIds.length > 0) {
%><%
for (var index in htmlWebpackPlugin.options.appMountIds) {
%><div id="<%= htmlWebpackPlugin.options.appMountIds[index]%>"></div><%
} %><%
} %><%
if (htmlWebpackPlugin.options.window) {
%><script>
<%
for (var varName in htmlWebpackPlugin.options.window) { %>window['<%=varName%>'] = <%= JSON.stringify(htmlWebpackPlugin.options.window[varName]) %>;<%
} %>
</script><%
} %><%
for (var chunk in htmlWebpackPlugin.files.chunks) {
%><script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script><%
} %><%
if (htmlWebpackPlugin.options.devServer) {
%><script src="<%= htmlWebpackPlugin.options.devServer%>/webpack-dev-server.js"></script><%
} %><%
if (htmlWebpackPlugin.options.googleAnalytics) {
%><script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
<% if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
ga('create', '<%= htmlWebpackPlugin.options.googleAnalytics.trackingId%>', 'auto');
<% } else { throw new Error("html-webpack-template requires googleAnalytics.trackingId config"); }%>
<% if (htmlWebpackPlugin.options.googleAnalytics.pageViewOnLoad) { %>
ga('send', 'pageview');
<% } %>
</script><%
}
%><!--<%= JSON.stringify(htmlWebpackPlugin)%>--></body>
</html>

5
js-frontend/public/robots.txt Executable file
View File

@@ -0,0 +1,5 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:

View File

@@ -0,0 +1,334 @@
/**
* React Select
* ============
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
* MIT License: https://github.com/keystonejs/react-select
*/
.Select {
position: relative;
}
.Select,
.Select div,
.Select input,
.Select span {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.Select.is-disabled > .Select-control {
background-color: #f9f9f9;
}
.Select.is-disabled > .Select-control:hover {
box-shadow: none;
}
.Select.is-disabled .Select-arrow-zone {
cursor: default;
pointer-events: none;
}
.Select-control {
background-color: #fff;
border-color: #d9d9d9 #ccc #b3b3b3;
border-radius: 4px;
border: 1px solid #ccc;
color: #333;
cursor: default;
display: table;
height: 36px;
outline: none;
overflow: hidden;
position: relative;
width: 100%;
}
.Select-control:hover {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}
.is-searchable.is-open > .Select-control {
cursor: text;
}
.is-open > .Select-control {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
background: #fff;
border-color: #b3b3b3 #ccc #d9d9d9;
}
.is-open > .Select-control > .Select-arrow {
border-color: transparent transparent #999;
border-width: 0 5px 5px;
}
.is-searchable.is-focused:not(.is-open) > .Select-control {
cursor: text;
}
.is-focused:not(.is-open) > .Select-control {
border-color: #007eff;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1);
}
.Select-placeholder,
:not(.Select--multi) > .Select-control .Select-value {
bottom: 0;
color: #aaa;
left: 0;
line-height: 34px;
padding-left: 10px;
padding-right: 10px;
position: absolute;
right: 0;
top: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label {
color: #333;
}
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label {
cursor: pointer;
text-decoration: none;
}
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
.has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus,
.has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus {
color: #007eff;
outline: none;
text-decoration: underline;
}
.Select-input {
height: 34px;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
}
.Select-input > input {
background: none transparent;
border: 0 none;
box-shadow: none;
cursor: default;
display: inline-block;
font-family: inherit;
font-size: inherit;
height: 34px;
margin: 0;
outline: none;
padding: 0;
-webkit-appearance: none;
}
.is-focused .Select-input > input {
cursor: text;
}
.has-value.is-pseudo-focused .Select-input {
opacity: 0;
}
.Select-control:not(.is-searchable) > .Select-input {
outline: none;
}
.Select-loading-zone {
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 16px;
}
.Select-loading {
-webkit-animation: Select-animation-spin 400ms infinite linear;
-o-animation: Select-animation-spin 400ms infinite linear;
animation: Select-animation-spin 400ms infinite linear;
width: 16px;
height: 16px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid #ccc;
border-right-color: #333;
display: inline-block;
position: relative;
vertical-align: middle;
}
.Select-clear-zone {
-webkit-animation: Select-animation-fadeIn 200ms;
-o-animation: Select-animation-fadeIn 200ms;
animation: Select-animation-fadeIn 200ms;
color: #999;
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 17px;
}
.Select-clear-zone:hover {
color: #D0021B;
}
.Select-clear {
display: inline-block;
font-size: 18px;
line-height: 1;
}
.Select--multi .Select-clear-zone {
width: 17px;
}
.Select-arrow-zone {
cursor: pointer;
display: table-cell;
position: relative;
text-align: center;
vertical-align: middle;
width: 25px;
padding-right: 5px;
}
.Select-arrow {
border-color: #999 transparent transparent;
border-style: solid;
border-width: 5px 5px 2.5px;
display: inline-block;
height: 0;
width: 0;
}
.is-open .Select-arrow,
.Select-arrow-zone:hover > .Select-arrow {
border-top-color: #666;
}
@-webkit-keyframes Select-animation-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes Select-animation-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.Select-menu-outer {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
border: 1px solid #ccc;
border-top-color: #e6e6e6;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
box-sizing: border-box;
margin-top: -1px;
max-height: 200px;
position: absolute;
top: 100%;
width: 100%;
z-index: 1;
-webkit-overflow-scrolling: touch;
}
.Select-menu {
max-height: 198px;
overflow-y: auto;
}
.Select-option {
box-sizing: border-box;
background-color: #fff;
color: #666666;
cursor: pointer;
display: block;
padding: 8px 10px;
}
.Select-option:last-child {
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
}
.Select-option.is-focused {
background-color: rgba(0, 126, 255, 0.08);
color: #333;
}
.Select-option.is-disabled {
color: #cccccc;
cursor: default;
}
.Select-noresults {
box-sizing: border-box;
color: #999999;
cursor: default;
display: block;
padding: 8px 10px;
}
.Select--multi .Select-input {
vertical-align: middle;
margin-left: 10px;
padding: 0;
}
.Select--multi.has-value .Select-input {
margin-left: 5px;
}
.Select--multi .Select-value {
background-color: rgba(0, 126, 255, 0.08);
border-radius: 2px;
border: 1px solid rgba(0, 126, 255, 0.24);
color: #007eff;
display: inline-block;
font-size: 0.9em;
line-height: 1.4;
margin-left: 5px;
margin-top: 5px;
vertical-align: top;
}
.Select--multi .Select-value-icon,
.Select--multi .Select-value-label {
display: inline-block;
vertical-align: middle;
}
.Select--multi .Select-value-label {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
cursor: default;
padding: 2px 5px;
}
.Select--multi a.Select-value-label {
color: #007eff;
cursor: pointer;
text-decoration: none;
}
.Select--multi a.Select-value-label:hover {
text-decoration: underline;
}
.Select--multi .Select-value-icon {
cursor: pointer;
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
border-right: 1px solid rgba(0, 126, 255, 0.24);
padding: 1px 5px 3px;
}
.Select--multi .Select-value-icon:hover,
.Select--multi .Select-value-icon:focus {
background-color: rgba(0, 113, 230, 0.08);
color: #0071e6;
}
.Select--multi .Select-value-icon:active {
background-color: rgba(0, 126, 255, 0.24);
}
.Select--multi.is-disabled .Select-value {
background-color: #fcfcfc;
border: 1px solid #e3e3e3;
color: #333;
}
.Select--multi.is-disabled .Select-value-icon {
cursor: not-allowed;
border-right: 1px solid #e3e3e3;
}
.Select--multi.is-disabled .Select-value-icon:hover,
.Select--multi.is-disabled .Select-value-icon:focus,
.Select--multi.is-disabled .Select-value-icon:active {
background-color: #fcfcfc;
}
@keyframes Select-animation-spin {
to {
transform: rotate(1turn);
}
}
@-webkit-keyframes Select-animation-spin {
to {
-webkit-transform: rotate(1turn);
}
}

View File

@@ -5,47 +5,29 @@
import React from "react";
import { createStore, compose, applyMiddleware, combineReducers} from "redux";
import { Provider, connect} from "react-redux";
import thunk from "redux-thunk";
import createLogger from 'redux-logger';
import { Route, IndexRoute, Link, IndexLink } from "react-router";
import { ReduxRouter} from "redux-router";
//import { Router, IndexRoute, Route, browserHistory } from 'react-router';
//import { syncHistory, routeReducer } from 'react-router-redux';
//import { configure as reduxAuthConfigure, authStateReducer } from "redux-auth";
//import { authStateReducer } from "redux-auth";
//import authStateReducer from './reducers/auth';
//import appStateReducer from './reducers/data';
import mainReducer from './reducers';
import { configure as reduxAuthConfigure } from './actions/configure';
//import { AuthGlobals } from "redux-auth/bootstrap-theme";
import { createHistory, createHashHistory, createMemoryHistory } from "history";
import { pushState, routerStateReducer, reduxReactRouter as clientRouter} from "redux-router";
import { reduxReactRouter as serverRouter } from "redux-router/server";
import { requireAuthentication } from './components/AuthComponent';
import mainReducer from './reducers';
//import demoButtons from "./reducers/request-test-buttons";
//import demoUi from "./reducers/demo-ui";
import { configure as endpointsConfig } from './actions/configure';
import { requireAuthentication } from './components/AuthComponent';
import Container from "./components/partials/Container";
import MyAccounts from "./views/MyAccounts";
import Account from "./views/Account";
import SignIn from "./views/SignIn";
import SignUp from "./views/SignUp";
//import GlobalComponents from "./views/partials/GlobalComponents";
class App extends React.Component {
render() {
return (
<Container>
return (<Container>
{this.props.children}
</Container>
);
</Container>);
}
}
@@ -82,15 +64,15 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {})
/**
* The React Router 1.0 routes for both the server and the client.
*/
return store.dispatch(reduxAuthConfigure([
return store.dispatch(endpointsConfig([
{
default: {
//apiUrl: '/',
emailSignInPath: '/login',
customersPath: '/customers',
currentUserPath: '/user',
accountsPath: '/accounts',
transfersPath: '/transfers'
emailSignInPath: '/api/login',
customersPath: '/api/customers',
currentUserPath: '/api/user',
accountsPath: '/api/accounts',
transfersPath: '/api/transfers'
}
}
], {

View File

@@ -2,65 +2,41 @@
* Created by andrew on 26/02/16.
*/
import T from '../constants/ACTION_TYPES';
import { makeActionCreator } from '../utils/actions';
import * as U from '../utils/sessionStorage';
import {
getCurrentSettings,
setCurrentSettings,
getInitialEndpointKey,
setDefaultEndpointKey,
setCurrentEndpoint,
setCurrentEndpointKey,
retrieveData,
persistData,
destroySession,
persistUserData,
retrieveUserData,
retrieveHeaders
} from "../utils/sessionStorage";
import {
apiGetCurrentUser
} from '../utils/api';
import {entityReceived } from './entities';
export function authenticateStart() {
return { type: T.AUTH.AUTHENTICATE_START };
}
export function authenticateComplete(user) {
return { type: T.AUTH.AUTHENTICATE_COMPLETE, user };
}
export function authenticateError(errors) {
return { type: T.AUTH.AUTHENTICATE_ERROR, errors };
}
import { apiGetCurrentUser } from '../utils/api';
import { entityReceived } from './entities';
export const authenticateStart = makeActionCreator(T.AUTH.AUTHENTICATE_START);
export const authenticateComplete = makeActionCreator(T.AUTH.AUTHENTICATE_COMPLETE, 'user');
export const authenticateError = makeActionCreator(T.AUTH.AUTHENTICATE_ERROR, 'errors');
export function authenticate(forceReread) {
return dispatch => {
return (dispatch) => {
dispatch(authenticateStart());
const savedUserPromise = new Promise((rs, rj) => {
const currentHeaders = retrieveHeaders();
const currentHeaders = U.retrieveHeaders();
const accessToken = currentHeaders["access-token"];
if (!accessToken) {
return rj({ reason: 'no token'});
}
const savedUser = retrieveUserData();
const savedUser = U.retrieveUserData();
if (savedUser && !forceReread) {
return rs(savedUser);
}
return apiGetCurrentUser().then((userData) => {
persistUserData(userData);
U.persistUserData(userData);
dispatch(entityReceived(userData.id, userData));
rs(userData);
}, (err) => {
debugger;
rj(err);
});

View File

@@ -5,6 +5,7 @@ import T from '../constants/ACTION_TYPES';
import { makeActionCreator } from '../utils/actions';
import * as api from '../utils/api';
import { authenticate } from './authenticate';
import root from '../utils/root';
export const entityRequested = makeActionCreator(T.ENTITIES.REQUESTED, 'id');
export const entityReceived = makeActionCreator(T.ENTITIES.RECEIVED, 'id', 'entity');
@@ -44,18 +45,47 @@ export function accountsList(userId) {
};
}
function readUntilChanged(initialData, customerId) {
const initialDataFlat = root['JSON'].stringify(initialData);
debugger;
return new Promise((rs, rj) => {
setTimeout(() => {
api.apiRetrieveAccounts(customerId)
.then(data => {
debugger;
if (initialDataFlat == root['JSON'].stringify(data)) {
return readUntilChanged.call(this, data, customerId).then(rs, rj); // Promise
}
rs(data);
})
.catch(rj)
}, 500);
})
}
export function accountCreate(customerId, payload) {
return dispatch => {
dispatch(accountCreateStart());
return api.apiCreateAccount(customerId, payload)
.then(({ accountId }) => {
dispatch(accountCreateComplete({
id: accountId,
...payload
}));
// dispatch(entityReceived(accountId, payload));
dispatch(authenticate(true));
return accountId;
.then(data => {
if (data.accountId) {
const { accountId } = data;
dispatch(accountCreateComplete({
id: accountId,
...payload
}));
// dispatch(entityReceived(accountId, payload));
dispatch(authenticate(true));
return accountId;
} else {
return readUntilChanged(data, customerId)
.then(() => {
dispatch(accountCreateComplete({
id: ''
}));
dispatch(authenticate(true));
});
}
})
.catch(err => {
debugger;
@@ -93,7 +123,7 @@ export function fetchOwnAccounts(customerId) {
//dispatch(accountsListRequested());
return api.apiRetrieveAccounts(customerId)
.then(data => {
dispatch(accountsListReceived(data));
dispatch(accountsListReceived(data.accounts));
});
};
}
@@ -217,20 +247,20 @@ export const makeTransfer = (accountId, payload) => {
};
};
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START);
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'payload');
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'error');
export const getTransfersRequested = makeActionCreator(T.TRANSFERS.LIST_START, 'id');
export const getTransfersComplete = makeActionCreator(T.TRANSFERS.LIST_COMPLETE, 'id', 'payload');
export const getTransfersError = makeActionCreator(T.TRANSFERS.LIST_ERROR, 'id', 'error');
export const getTransfers = (accountId) => {
return dispatch => {
dispatch(getTransfersRequested());
dispatch(getTransfersRequested(accountId));
return api.apiRetrieveTransfers(accountId)
.then(data => {
dispatch(getTransfersComplete(data));
dispatch(getTransfersComplete(accountId, data.transactionsHistory));
return data;
})
.catch(err => {
dispatch(getTransfersError(err));
dispatch(getTransfersError(accountId, err));
return err;
});
};

View File

@@ -4,12 +4,10 @@
import React from "react";
import ReactDOM from "react-dom";
import { initialize } from "./app";
/**
* Fire-up React Router.
*/
const reactRoot = window.document.getElementById("react-app");
const reactRoot = window.document.getElementById("root");
initialize().then(({ provider }) => {
ReactDOM.render(provider, reactRoot);
});

View File

@@ -4,8 +4,9 @@
import React from "react";
import { connect } from 'react-redux';
import Spinner from "react-loader";
import * as BS from "react-bootstrap";
// import * as BS from "react-bootstrap";
import * as A from '../actions/entities';
import { Route, IndexRoute, Link, IndexLink } from "react-router";
// import { Money } from '../components/Money';
@@ -32,15 +33,15 @@ export class AccountInfo extends React.Component {
const account = entities[accountId];
if (!account) {
return (<div>{ accountId } <Spinner ref="spinner" loaded={false} /></div>)
return (<Link to={ `/account/${accountId}` }>{ accountId } <Spinner loaded={false} /></Link>)
}
const { title } = account;
return (<div>{ title } </div>);
return (<Link to={ `/account/${accountId}` }>{ title }</Link>);
}
}
export default connect(({ app }) => ({
entities: app.data.entities
}))(AccountInfo);
}))(AccountInfo);

View File

@@ -11,49 +11,99 @@ import AccountInfo from './AccountInfo';
export class TransfersTable extends React.Component {
render() {
const { loading, data, errors } = this.props;
if (loading) {
return (<h2><Spinner ref="spinner" loaded={false} /> Loading..</h2>);
const { transfers, forAccount } = this.props;
const { loading, data, errors } = transfers || {};
if (!transfers || loading) {
return (<h2><Spinner loaded={false} /> Loading..</h2>);
}
if (Object.keys(errors).length) {
return (<div className="text-danger">Errors..</div>);
}
const transfers = data.length ? data.map(({
amount,
fromAccountId,
toAccountId,
transactionId,
description = '',
date = null,
status = ''
}, idx) => (<tr key={idx}>
<td><TimeAgo date={date} /></td>
<td><AccountInfo accountId={ fromAccountId } /></td>
<td><AccountInfo accountId={ toAccountId } /></td>
<td><Money amount={ amount } /></td>
<td>{ description || 'N/a'}</td>
<td>{ status || 'N/a' }</td>
</tr>)) : (<tr>
<td colSpan={6}>No transfers for this account just yet.</td>
</tr>);
const currentAccountId = forAccount;
const transfersMarkup = data.length ?
data
.sort((a, b) => ((a.date - b.date)))
.filter(({ entryType, toAccountId, fromAccountId}) => ((entryType !=='transaction') || (fromAccountId === currentAccountId) || (toAccountId === currentAccountId)))
.reduce(({
items, balance
}, v) => {
if (v.entryType == 'account') {
balance = v.initialBalance;
} else if (v.entryType == 'transaction') {
const isOriginating = v.fromAccountId == currentAccountId;
balance += (isOriginating ? -1 : 1) * v.amount;
}
v.balance = balance;
items.push(v);
return { items, balance };
}, {
items: [],
balance: 0
}).items
.sort((a, b) => (-(a.date - b.date)))
.map(({
entryType,
amount,
fromAccountId,
toAccountId,
transactionId,
description = '—',
date = null,
status = '—',
balance,
initialBalance = null
}) => {
const transferTimestamp = new Date(date);
const timeAgoTitle = transferTimestamp.toLocaleDateString() + ' ' + transferTimestamp.toLocaleTimeString();
if (entryType == 'account') {
return (<tr>
<td><TimeAgo date={date} title={ timeAgoTitle } /></td>
<td colSpan="3">Account created</td>
<td><Money amount={ initialBalance } /></td>
<td></td>
<td>{ status || '—' }</td>
</tr>);
}
const isOriginating = fromAccountId == currentAccountId;
const directionMarkup = isOriginating ? 'Debit' : 'Credit';
const counterAccountMarkup = isOriginating ?
<AccountInfo accountId={ toAccountId } /> :
<AccountInfo accountId={ fromAccountId } />;
return (<tr>
<td><TimeAgo date={date} title={ timeAgoTitle } /></td>
<td>{ directionMarkup }</td>
<td>{ counterAccountMarkup }</td>
<td><Money amount={ amount } /></td>
<td><Money amount={ balance } /></td>
<td>{ description || '—' }</td>
<td>{ status || '—' }</td>
</tr>);
}) : (<tr>
<td colSpan={6}>No transfers for this account just yet.</td>
</tr>);
return (
<BS.Table striped bordered condensed hover>
<thead>
<tr>
<th>Date</th>
<th>What</th>
<th>Counter Account</th>
<th>Type</th>
<th>Other Account</th>
<th>Amount</th>
<th>Balance</th>
<th>Description</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{ transfers }
{ transfersMarkup }
</tbody>
</BS.Table>
);

View File

@@ -11,11 +11,6 @@ import HeaderLinks from '../HeaderLinks';
//const SignOutButton = () => (<div>SignOutButton!</div>);
//if (!global.__SERVER__ && !global.__TEST__) {
// require("../../styles/main.scss");
//}
class Container extends React.Component {
static propTypes = {
children: PropTypes.node

View File

@@ -1,81 +1,81 @@
import defineActionTypes from '../utils/defineActionTypes'
import { TODO_DEFINE, defineActionType } from '../utils/defineActionTypes'
export default defineActionTypes({
export default defineActionType({
/*
* View model
*/
AUTH: `
CONFIGURE_START
CONFIGURE_COMPLETE
CONFIGURE_ERROR
AUTHENTICATE_START
AUTHENTICATE_COMPLETE
AUTHENTICATE_ERROR
SIGN_IN_START
SIGN_IN_COMPLETE
SIGN_IN_ERROR
SIGN_IN_FORM_UPDATE
SIGN_UP_START
SIGN_UP_COMPLETE
SIGN_UP_ERROR
SIGN_UP_FORM_UPDATE
SIGN_OUT_START
SIGN_OUT_COMPLETE
`,
AUTH: {
CONFIGURE_START: TODO_DEFINE,
CONFIGURE_COMPLETE: TODO_DEFINE,
CONFIGURE_ERROR: TODO_DEFINE,
AUTHENTICATE_START: TODO_DEFINE,
AUTHENTICATE_COMPLETE: TODO_DEFINE,
AUTHENTICATE_ERROR: TODO_DEFINE,
SIGN_IN_START: TODO_DEFINE,
SIGN_IN_COMPLETE: TODO_DEFINE,
SIGN_IN_ERROR: TODO_DEFINE,
SIGN_IN_FORM_UPDATE: TODO_DEFINE,
SIGN_UP_START: TODO_DEFINE,
SIGN_UP_COMPLETE: TODO_DEFINE,
SIGN_UP_ERROR: TODO_DEFINE,
SIGN_UP_FORM_UPDATE: TODO_DEFINE,
SIGN_OUT_START: TODO_DEFINE,
SIGN_OUT_COMPLETE: TODO_DEFINE
},
ENTITIES: `
REQUESTED
RECEIVED
RECEIVED_LIST
`,
ENTITIES: {
REQUESTED: TODO_DEFINE,
RECEIVED: TODO_DEFINE,
RECEIVED_LIST: TODO_DEFINE
},
ACCOUNTS: `
LIST_START
LIST_COMPLETE
LIST_ERROR
LIST_REF_START
LIST_REF_COMPLETE
LIST_REF_ERROR
CREATE_START
CREATE_COMPLETE
CREATE_ERROR
CREATE_FORM_UPDATE
EDIT_START
EDIT_COMPLETE
EDIT_ERROR
EDIT_FORM_UPDATE
CREATE_REF_START
CREATE_REF_COMPLETE
CREATE_REF_ERROR
CREATE_REF_FORM_UPDATE
CREATE_REF_OWNER_LOOKUP_START
CREATE_REF_OWNER_LOOKUP_COMPLETE
CREATE_REF_ACCOUNT_LOOKUP_START
CREATE_REF_ACCOUNT_LOOKUP_COMPLETE
`,
ACCOUNTS: {
LIST_START: TODO_DEFINE,
LIST_COMPLETE: TODO_DEFINE,
LIST_ERROR: TODO_DEFINE,
LIST_REF_START: TODO_DEFINE,
LIST_REF_COMPLETE: TODO_DEFINE,
LIST_REF_ERROR: TODO_DEFINE,
CREATE_START: TODO_DEFINE,
CREATE_COMPLETE: TODO_DEFINE,
CREATE_ERROR: TODO_DEFINE,
CREATE_FORM_UPDATE: TODO_DEFINE,
EDIT_START: TODO_DEFINE,
EDIT_COMPLETE: TODO_DEFINE,
EDIT_ERROR: TODO_DEFINE,
EDIT_FORM_UPDATE: TODO_DEFINE,
CREATE_REF_START: TODO_DEFINE,
CREATE_REF_COMPLETE: TODO_DEFINE,
CREATE_REF_ERROR: TODO_DEFINE,
CREATE_REF_FORM_UPDATE: TODO_DEFINE,
CREATE_REF_OWNER_LOOKUP_START: TODO_DEFINE,
CREATE_REF_OWNER_LOOKUP_COMPLETE: TODO_DEFINE,
CREATE_REF_ACCOUNT_LOOKUP_START: TODO_DEFINE,
CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: TODO_DEFINE
},
ACCOUNT: `
SINGLE_START
SINGLE_COMPLETE
SINGLE_ERROR
DELETE_START
DELETE_COMPLETE
DELETE_ERROR
`,
ACCOUNT: {
SINGLE_START: TODO_DEFINE,
SINGLE_COMPLETE: TODO_DEFINE,
SINGLE_ERROR: TODO_DEFINE,
DELETE_START: TODO_DEFINE,
DELETE_COMPLETE: TODO_DEFINE,
DELETE_ERROR: TODO_DEFINE
},
TRANSFERS: `
MAKE_START
MAKE_COMPLETE
MAKE_ERROR
MAKE_FORM_UPDATE
LIST_START
LIST_COMPLETE
LIST_ERROR
`,
TRANSFERS: {
MAKE_START: TODO_DEFINE,
MAKE_COMPLETE: TODO_DEFINE,
MAKE_ERROR: TODO_DEFINE,
MAKE_FORM_UPDATE: TODO_DEFINE,
LIST_START: TODO_DEFINE,
LIST_COMPLETE: TODO_DEFINE,
LIST_ERROR: TODO_DEFINE
},
ERROR: `
START
STOP
`
ERROR: {
START:TODO_DEFINE,
STOP:TODO_DEFINE
}
})

Some files were not shown because too many files have changed in this diff Show More