diff --git a/README.md b/README.md
new file mode 100644
index 0000000..332059a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# Rozwój kodu
+
+Kod będzie rozwijać się wraz z cotygodniową narracją szkoleniową.
+Zarówno pojawiać się w nim będą kolejne poprawki jak i odziedziczone po firmach partnerskich nowe moduły ;-) Jak to w prawdziwym legacy.
+
+# Przeglądanie kodu
+
+Poszczególne kroki refaktoryzacyjne najlepiej przeglądać używająć tagów. Każdy krok szkoleniowy, który opisany jest w odcinku Legacy Fighter posiada na końcu planszę z nazwą odpowiedniego taga. Porównać zmiany można robiąc diffa w stosunku do poprzedniego taga z narracji.
diff --git a/pom.xml b/pom.xml
index e2d45c3..28f755f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,6 +19,11 @@
spring-boot-starter-web
+
+ org.togglz
+ togglz-spring-boot-starter
+ 2.6.1.Final
+
org.springframework.boot
@@ -43,6 +48,12 @@
test
+
+ org.togglz
+ togglz-junit5
+ 2.6.1.Final
+ test
+
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/acme/dynamic/DocumentOperationResult.java b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/dynamic/DocumentOperationResult.java
new file mode 100644
index 0000000..87ad54f
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/dynamic/DocumentOperationResult.java
@@ -0,0 +1,69 @@
+package io.legacyfighter.cabs.contracts.application.acme.dynamic;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+
+import java.util.List;
+import java.util.Map;
+
+public class DocumentOperationResult {
+
+ public enum Result{
+ SUCCESS, ERROR
+ }
+
+ private Result result;
+ private String stateName;
+ private ContentId contentId;
+
+ private Long documentHeaderId;
+ private DocumentNumber documentNumber;
+
+ private Map> possibleTransitionsAndRules;
+ private boolean contentChangePossible;
+ private String contentChangePredicate;
+
+
+ public DocumentOperationResult(Result result, Long documentHeaderId, DocumentNumber documentNumber, String stateName, ContentId contentId, Map> possibleTransitionsAndRules, boolean contentChangePossible, String contentChangePredicate) {
+ this.result = result;
+ this.documentHeaderId = documentHeaderId;
+ this.documentNumber = documentNumber;
+ this.stateName = stateName;
+ this.contentId = contentId;
+ this.possibleTransitionsAndRules = possibleTransitionsAndRules;
+ this.contentChangePossible = contentChangePossible;
+ this.contentChangePredicate = contentChangePredicate;
+ }
+
+ public Map> getPossibleTransitionsAndRules() {
+ return possibleTransitionsAndRules;
+ }
+
+ public String getContentChangePredicate() {
+ return contentChangePredicate;
+ }
+
+ public boolean isContentChangePossible() {
+ return contentChangePossible;
+ }
+
+ public Result getResult() {
+ return result;
+ }
+
+ public String getStateName() {
+ return stateName;
+ }
+
+ public DocumentNumber getDocumentNumber() {
+ return documentNumber;
+ }
+
+ public Long getDocumentHeaderId() {
+ return documentHeaderId;
+ }
+
+ public ContentId getContentId() {
+ return contentId;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/acme/dynamic/DocumentResourceManager.java b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/dynamic/DocumentResourceManager.java
new file mode 100644
index 0000000..9475095
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/dynamic/DocumentResourceManager.java
@@ -0,0 +1,111 @@
+package io.legacyfighter.cabs.contracts.application.acme.dynamic;
+
+import io.legacyfighter.cabs.contracts.legacy.User;
+import io.legacyfighter.cabs.contracts.legacy.UserRepository;
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.DocumentHeaderRepository;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.StateConfig;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.acme.AcmeContractStateAssembler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.function.BiFunction;
+
+@Service
+@Transactional
+public class DocumentResourceManager {
+
+ @Autowired
+ private DocumentHeaderRepository documentHeaderRepository;
+
+ @Autowired
+ private AcmeContractStateAssembler assembler;
+
+ @Autowired
+ private UserRepository userRepository;
+
+ public void changeContent(){
+
+ }
+
+ public DocumentOperationResult createDocument(Long authorId){
+ User author = userRepository.getOne(authorId);
+
+ DocumentNumber number = generateNumber();
+ DocumentHeader documentHeader = new DocumentHeader(author.getId(), number);
+
+ StateConfig stateConfig = assembler.assemble();
+ State state = stateConfig.begin(documentHeader);
+
+ documentHeaderRepository.save(documentHeader);
+
+ return generateDocumentOperationResult(DocumentOperationResult.Result.SUCCESS, state);
+ }
+
+ public DocumentOperationResult changeState(Long documentId, String desiredState, Map params){
+ DocumentHeader documentHeader = documentHeaderRepository.getOne(documentId);
+ StateConfig stateConfig = assembler.assemble();
+ State state = stateConfig.recreate(documentHeader);
+
+ state = state.changeState(new ChangeCommand(desiredState, params));
+
+ documentHeaderRepository.save(documentHeader);
+
+ return generateDocumentOperationResult(DocumentOperationResult.Result.SUCCESS, state);
+ }
+
+ public DocumentOperationResult changeContent(Long headerId, ContentId contentVersion) {
+ DocumentHeader documentHeader = documentHeaderRepository.getOne(headerId);
+ StateConfig stateConfig = assembler.assemble();
+ State state = stateConfig.recreate(documentHeader);
+ state = state.changeContent(contentVersion);
+
+ documentHeaderRepository.save(documentHeader);
+ return generateDocumentOperationResult(DocumentOperationResult.Result.SUCCESS, state);
+ }
+
+ private DocumentOperationResult generateDocumentOperationResult(DocumentOperationResult.Result result, State state) {
+ return new DocumentOperationResult(result, state.getDocumentHeader().getId(),
+ state.getDocumentHeader().getDocumentNumber(), state.getStateDescriptor(), state.getDocumentHeader().getContentId(),
+ extractPossibleTransitionsAndRules(state),
+ state.isContentEditable(),
+ extractContentChangePredicate(state));
+ }
+
+ private String extractContentChangePredicate(State state) {
+ if (state.isContentEditable())
+ return state.getContentChangePredicate().getClass().getTypeName();
+ return null;
+ }
+
+
+ private Map> extractPossibleTransitionsAndRules(State state) {
+ Map> transitionsAndRules = new HashMap<>();
+
+ Map>> stateChangePredicates = state.getStateChangePredicates();
+ for (State s : stateChangePredicates.keySet()){
+ //transition to self is not important
+ if (s.equals(state))
+ continue;
+
+ List> predicates = stateChangePredicates.get(s);
+ List ruleNames = new ArrayList<>();
+ for (BiFunction predicate : predicates){
+ ruleNames.add(predicate.getClass().getTypeName());
+ }
+ transitionsAndRules.put(s.getStateDescriptor(), ruleNames);
+ }
+
+ return transitionsAndRules;
+ }
+
+ private DocumentNumber generateNumber() {
+ return new DocumentNumber("nr: " + new Random().nextInt()); //TODO integrate with doc number generator
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/AcmeContractProcessBasedOnStraightforwardDocumentModel.java b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/AcmeContractProcessBasedOnStraightforwardDocumentModel.java
new file mode 100644
index 0000000..97c17e3
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/AcmeContractProcessBasedOnStraightforwardDocumentModel.java
@@ -0,0 +1,67 @@
+package io.legacyfighter.cabs.contracts.application.acme.straigthforward;
+
+import io.legacyfighter.cabs.contracts.legacy.User;
+import io.legacyfighter.cabs.contracts.legacy.UserRepository;
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.DocumentHeaderRepository;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.acme.VerifiedState;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Random;
+
+@Service
+@Transactional
+public class AcmeContractProcessBasedOnStraightforwardDocumentModel {
+ @Autowired
+ private UserRepository userRepository;
+
+ @Autowired
+ private DocumentHeaderRepository documentHeaderRepository;
+
+ @Autowired
+ private AcmeStateFactory stateFactory;
+
+ public ContractResult createContract(Long authorId){
+ User author = userRepository.getOne(authorId);
+
+ DocumentNumber number = generateNumber();
+ DocumentHeader header = new DocumentHeader(author.getId(), number);
+
+ documentHeaderRepository.save(header);
+
+ return new ContractResult(ContractResult.Result.SUCCESS, header.getId(), number, header.getStateDescriptor());
+ }
+
+
+ public ContractResult verify(Long headerId, Long verifierId) {
+ User verifier = userRepository.getOne(verifierId);
+ //TODO user authorization
+
+ DocumentHeader header = documentHeaderRepository.getOne(headerId);
+
+ BaseState state = stateFactory.create(header);
+ state = state.changeState(new VerifiedState(verifierId));
+
+ documentHeaderRepository.save(header);
+ return new ContractResult(ContractResult.Result.SUCCESS, headerId, header.getDocumentNumber(), header.getStateDescriptor());
+ }
+
+ public ContractResult changeContent(Long headerId, ContentId contentVersion) {
+ DocumentHeader header = documentHeaderRepository.getOne(headerId);
+
+ BaseState state = stateFactory.create(header);
+ state = state.changeContent(contentVersion);
+
+ documentHeaderRepository.save(header);
+ return new ContractResult(ContractResult.Result.SUCCESS, headerId, header.getDocumentNumber(), header.getStateDescriptor());
+ }
+
+ private DocumentNumber generateNumber() {
+ return new DocumentNumber("nr: " + new Random().nextInt()); //TODO integrate with doc number generator
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/AcmeStateFactory.java b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/AcmeStateFactory.java
new file mode 100644
index 0000000..5f62fec
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/AcmeStateFactory.java
@@ -0,0 +1,31 @@
+package io.legacyfighter.cabs.contracts.application.acme.straigthforward;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.acme.DraftState;
+import org.springframework.stereotype.Component;
+
+@Component
+class AcmeStateFactory {
+ public BaseState create(DocumentHeader header){
+ //sample impl is based on class names
+ //other possibilities: names Dependency Injection Containers, states persisted via ORM Discriminator mechanism, mapper
+ String className = header.getStateDescriptor();
+
+ if (className == null) {
+ DraftState state = new DraftState();
+ state.init(header);
+ return state;
+ }
+
+ try {
+ Class extends BaseState> clazz = (Class extends BaseState>) Class.forName(className);
+ BaseState state = clazz.getConstructor().newInstance();
+ state.init(header);
+ return state;
+ }
+ catch (Exception e){
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/ContractResult.java b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/ContractResult.java
new file mode 100644
index 0000000..4c89cea
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/acme/straigthforward/ContractResult.java
@@ -0,0 +1,38 @@
+package io.legacyfighter.cabs.contracts.application.acme.straigthforward;
+
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+
+public class ContractResult {
+
+ public enum Result{
+ FAILURE, SUCCESS
+ }
+
+ private Result result;
+ private Long documentHeaderId;
+ private DocumentNumber documentNumber;
+ private String stateDescriptor;
+
+ public ContractResult(Result result, Long documentHeaderId, DocumentNumber documentNumber, String stateDescriptor) {
+ this.result = result;
+ this.documentHeaderId = documentHeaderId;
+ this.documentNumber = documentNumber;
+ this.stateDescriptor = stateDescriptor;
+ }
+
+ public Result getResult() {
+ return result;
+ }
+
+ public DocumentNumber getDocumentNumber() {
+ return documentNumber;
+ }
+
+ public Long getDocumentHeaderId() {
+ return documentHeaderId;
+ }
+
+ public String getStateDescriptor() {
+ return stateDescriptor;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/editor/CommitResult.java b/src/main/java/io/legacyfighter/cabs/contracts/application/editor/CommitResult.java
new file mode 100644
index 0000000..3904b12
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/editor/CommitResult.java
@@ -0,0 +1,33 @@
+package io.legacyfighter.cabs.contracts.application.editor;
+
+import java.util.UUID;
+
+public class CommitResult {
+
+
+ public enum Result{
+ FAILURE, SUCCESS
+ }
+
+ private UUID contentId;
+ private Result result;
+ private String message;
+
+ public CommitResult(UUID contentId, Result result, String message) {
+ this.contentId = contentId;
+ this.result = result;
+ this.message = message;
+ }
+
+ public CommitResult(UUID documentId, Result result) {
+ this(documentId, result, null);
+ }
+
+ public Result getResult() {
+ return result;
+ }
+
+ public UUID getContentId() {
+ return contentId;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/editor/DocumentDTO.java b/src/main/java/io/legacyfighter/cabs/contracts/application/editor/DocumentDTO.java
new file mode 100644
index 0000000..1bef7a0
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/editor/DocumentDTO.java
@@ -0,0 +1,29 @@
+package io.legacyfighter.cabs.contracts.application.editor;
+
+import io.legacyfighter.cabs.contracts.model.content.ContentVersion;
+
+import java.util.UUID;
+
+public class DocumentDTO {
+ private final UUID contentId;
+ private final String physicalContent;
+ private final ContentVersion contentVersion;
+
+ public DocumentDTO(UUID contentId, String physicalContent, ContentVersion contentVersion) {
+ this.contentId = contentId;
+ this.physicalContent = physicalContent;
+ this.contentVersion = contentVersion;
+ }
+
+ public UUID getContentId() {
+ return contentId;
+ }
+
+ public ContentVersion getDocumentVersion() {
+ return contentVersion;
+ }
+
+ public String getPhysicalContent() {
+ return physicalContent;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/application/editor/DocumentEditor.java b/src/main/java/io/legacyfighter/cabs/contracts/application/editor/DocumentEditor.java
new file mode 100644
index 0000000..6ef2fc0
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/application/editor/DocumentEditor.java
@@ -0,0 +1,30 @@
+package io.legacyfighter.cabs.contracts.application.editor;
+
+import io.legacyfighter.cabs.contracts.model.content.DocumentContent;
+import io.legacyfighter.cabs.contracts.model.content.DocumentContentRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+@Service
+@Transactional
+public class DocumentEditor {
+ @Autowired
+ private DocumentContentRepository documentContentRepository;
+
+ public CommitResult commit(DocumentDTO document){
+ UUID previousID = document.getContentId();
+ DocumentContent content = new DocumentContent(previousID, document.getDocumentVersion(), document.getPhysicalContent());
+ documentContentRepository.save(content);
+ return new CommitResult(content.getId(), CommitResult.Result.SUCCESS);
+ }
+
+
+ public DocumentDTO get(UUID contentId){
+ DocumentContent content = documentContentRepository.getOne(contentId);
+ return new DocumentDTO(contentId, content.getPhysicalContent(), content.getDocumentVersion());
+ }
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/infra/JpaDocumentHeaderRepository.java b/src/main/java/io/legacyfighter/cabs/contracts/infra/JpaDocumentHeaderRepository.java
new file mode 100644
index 0000000..c8033d7
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/infra/JpaDocumentHeaderRepository.java
@@ -0,0 +1,30 @@
+package io.legacyfighter.cabs.contracts.infra;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.DocumentHeaderRepository;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.EntityManager;
+import javax.persistence.LockModeType;
+import javax.persistence.PersistenceContext;
+
+//LockModeType surprised you? MUST see: https://youtu.be/uj25PbkHb94?t=499
+
+@Repository
+public class JpaDocumentHeaderRepository implements DocumentHeaderRepository {
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Override
+ public DocumentHeader getOne(Long id){
+ return entityManager.find(DocumentHeader.class, id, LockModeType.OPTIMISTIC);
+ }
+
+ @Override
+ public void save(DocumentHeader header) {
+ if (entityManager.contains(header))
+ entityManager.lock(header, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
+ else
+ entityManager.persist(header);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/BaseAggregateRoot.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/BaseAggregateRoot.java
new file mode 100644
index 0000000..977ea89
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/BaseAggregateRoot.java
@@ -0,0 +1,10 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+import io.legacyfighter.cabs.common.BaseEntity;
+
+public abstract class BaseAggregateRoot extends BaseEntity {
+ //just to be DDD compliant
+
+ //if you have added a DDD skill to your linkedin profile just because extending this class add + below:
+ //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/Contract2.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Contract2.java
new file mode 100644
index 0000000..202a670
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Contract2.java
@@ -0,0 +1,51 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+import java.util.Random;
+
+public class Contract2 extends Document implements Versionable{
+
+ public Contract2(String number, User creator) {
+ super(number, creator);
+ }
+
+ @Override
+ public void publish() throws UnsupportedTransitionException {
+ throw new UnsupportedTransitionException(status, DocumentStatus.PUBLISHED);
+ }
+
+ public void accept(){
+ if (status == DocumentStatus.VERIFIED){
+ status = DocumentStatus.PUBLISHED; //reusing unused enum to provide data model for new status
+ }
+ }
+
+ //Contracts just don't have a title, it's just a part of the content
+ @Override
+ public void changeTitle(String title) {
+ super.changeContent(title + getContent());
+ }
+
+ //NOT @Override
+ public void changeContent(String content, String userStatus){
+ if (userStatus == "ChiefSalesOfficerStatus" || misterVladimirIsLoggedIn(userStatus)){
+ overridePublished = true;
+ changeContent(content);
+ }
+ }
+
+ private boolean misterVladimirIsLoggedIn(String userStatus) {
+ return userStatus.toLowerCase().trim().equals("!!!id=" + NUMBER_OF_THE_BEAST);
+ }
+
+ private static final String NUMBER_OF_THE_BEAST = "616";
+
+ @Override
+ public void recreateTo(long version) {
+ //TODO need to learn Kafka
+ }
+
+ @Override
+ public long getLastVersion() {
+ return new Random().nextLong();//FIXME, don't know how to return a null
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/Document.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Document.java
new file mode 100644
index 0000000..968b4eb
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Document.java
@@ -0,0 +1,88 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+
+import javax.persistence.*;
+import java.util.Set;
+
+@Entity
+public class Document extends BaseAggregateRoot implements Printable {
+ private String number;
+ private String title;
+ private String content;
+
+ @Enumerated(EnumType.STRING)
+ protected DocumentStatus status = DocumentStatus.DRAFT;
+
+ @ManyToMany
+ private Set assignedUsers;
+
+ @ManyToOne
+ private User creator;
+ @ManyToOne
+ private User verifier;
+
+ public Document(String number, User creator){
+ this.number = number;
+ this.creator = creator;
+ }
+
+ protected Document() {}
+
+ public void verifyBy(User verifier){
+ if (status != DocumentStatus.DRAFT)
+ throw new IllegalStateException("Can not verify in status: " + status);
+ if (creator.equals(verifier))
+ throw new IllegalArgumentException("Verifier can not verify documents by himself");
+ this.verifier = verifier;
+ status = DocumentStatus.VERIFIED;
+ }
+
+ public void publish() throws UnsupportedTransitionException {//code open for modifications: throws is for super classes
+ if (status != DocumentStatus.VERIFIED)
+ throw new IllegalStateException("Can not publish in status: " + status);
+ status = DocumentStatus.PUBLISHED;
+ }
+
+ public void archive(){
+ status = DocumentStatus.ARCHIVED;
+ }
+
+ //===============================================================
+
+ public void changeTitle(String title){
+ if (status == DocumentStatus.ARCHIVED || status == DocumentStatus.PUBLISHED)
+ throw new IllegalStateException("Can not change title in status: " + status);
+ this.title = title;
+ if (status == DocumentStatus.VERIFIED)
+ status = DocumentStatus.DRAFT;
+ }
+
+ protected boolean overridePublished;
+
+ public void changeContent(String content){
+ if (overridePublished){
+ this.content = content;
+ return;
+ }
+
+ if (status == DocumentStatus.ARCHIVED || status == DocumentStatus.PUBLISHED)
+ throw new IllegalStateException("Can not change content in status: " + status);
+ this.content = content;
+ if (status == DocumentStatus.VERIFIED)
+ status = DocumentStatus.DRAFT;
+ }
+
+ //===============================================================
+
+ protected String getContent() {
+ return content;
+ }
+
+ public DocumentStatus getStatus() {
+ return status;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/DocumentStatus.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/DocumentStatus.java
new file mode 100644
index 0000000..0febf38
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/DocumentStatus.java
@@ -0,0 +1,5 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+public enum DocumentStatus {
+ DRAFT, VERIFIED, PUBLISHED, ARCHIVED
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/OOParadigm.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/OOParadigm.java
new file mode 100644
index 0000000..6c20e10
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/OOParadigm.java
@@ -0,0 +1,23 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+abstract class OOParadigm {
+ //2. enkapsulacja - ukrycie impl
+ private Object filed;
+
+ //1. abstrakcja - agent odbierający sygnały
+ public void method(){
+ //do sth
+ }
+
+ //3. polimorfizm - zmienne zachowania
+ protected abstract void abstractStep();
+}
+
+//4. dziedziczenie - technika wspierająca polimorizm
+class ConcreteType extends OOParadigm{
+
+ @Override
+ protected void abstractStep() {
+
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/Printable.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Printable.java
new file mode 100644
index 0000000..5623e5f
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Printable.java
@@ -0,0 +1,8 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+/**
+ * Marker interface. Corporate standard.
+ */
+public interface Printable {
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/UnsupportedTransitionException.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/UnsupportedTransitionException.java
new file mode 100644
index 0000000..e3d0e24
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/UnsupportedTransitionException.java
@@ -0,0 +1,20 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+public class UnsupportedTransitionException extends RuntimeException{
+ private DocumentStatus current;
+ private DocumentStatus desired;
+
+ public UnsupportedTransitionException(DocumentStatus current, DocumentStatus desired) {
+ super("can not transit form " + current + " to " + desired);
+ this.current = current;
+ this.desired = desired;
+ }
+
+ public DocumentStatus getCurrent() {
+ return current;
+ }
+
+ public DocumentStatus getDesired() {
+ return desired;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/User.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/User.java
new file mode 100644
index 0000000..ecdcb28
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/User.java
@@ -0,0 +1,9 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+import io.legacyfighter.cabs.common.BaseEntity;
+
+import javax.persistence.Entity;
+
+@Entity
+public class User extends BaseEntity {
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/UserRepository.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/UserRepository.java
new file mode 100644
index 0000000..2052bcb
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/UserRepository.java
@@ -0,0 +1,6 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserRepository extends JpaRepository {
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/legacy/Versionable.java b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Versionable.java
new file mode 100644
index 0000000..8732bd7
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/legacy/Versionable.java
@@ -0,0 +1,6 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+public interface Versionable {
+ void recreateTo(long version);
+ long getLastVersion();
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/ContentId.java b/src/main/java/io/legacyfighter/cabs/contracts/model/ContentId.java
new file mode 100644
index 0000000..75f0710
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/ContentId.java
@@ -0,0 +1,36 @@
+package io.legacyfighter.cabs.contracts.model;
+
+import javax.persistence.Embeddable;
+import java.util.Objects;
+import java.util.UUID;
+
+@Embeddable
+public class ContentId {
+ private UUID contentId;
+
+ protected ContentId(){}
+
+ public ContentId(UUID contentId){
+ this.contentId = contentId;
+ }
+
+ @Override
+ public String toString() {
+ return "ContentId{" +
+ "contentId=" + contentId +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ContentId contentId1 = (ContentId) o;
+ return contentId.equals(contentId1.contentId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(contentId);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/DocumentHeader.java b/src/main/java/io/legacyfighter/cabs/contracts/model/DocumentHeader.java
new file mode 100644
index 0000000..a214610
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/DocumentHeader.java
@@ -0,0 +1,74 @@
+package io.legacyfighter.cabs.contracts.model;
+
+import io.legacyfighter.cabs.common.BaseEntity;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+
+import javax.persistence.Embedded;
+import javax.persistence.Entity;
+
+@Entity
+public class DocumentHeader extends BaseEntity {
+ @Embedded
+ private DocumentNumber number;
+
+ private Long authorId;
+
+ private Long verifierId;
+
+ private String stateDescriptor;
+
+ @Embedded
+ private ContentId contentId;
+
+ protected DocumentHeader(){
+
+ }
+
+ public DocumentHeader(Long authorId, DocumentNumber number){
+ this.authorId = authorId;
+ this.number = number;
+ }
+
+ public void changeCurrentContent(ContentId contentId){
+ this.contentId = contentId;
+ }
+
+ public boolean notEmpty() {
+ return contentId != null;
+ }
+
+
+ public Long getVerifier() {
+ return verifierId;
+ }
+
+ public Long getAuthorId() {
+ return authorId;
+ }
+
+ public void setVerifierId(Long verifierId) {
+ this.verifierId = verifierId;
+ }
+
+ public void setAuthorId(Long authorId) {
+ this.authorId = authorId;
+ }
+
+ public String getStateDescriptor() {
+ return stateDescriptor;
+ }
+
+ public void setStateDescriptor(String stateDescriptor) {
+ this.stateDescriptor = stateDescriptor;
+ }
+
+ public DocumentNumber getDocumentNumber() {
+ return number;
+ }
+
+ public ContentId getContentId() {
+ return contentId;
+ }
+
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/DocumentHeaderRepository.java b/src/main/java/io/legacyfighter/cabs/contracts/model/DocumentHeaderRepository.java
new file mode 100644
index 0000000..315ca6c
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/DocumentHeaderRepository.java
@@ -0,0 +1,8 @@
+package io.legacyfighter.cabs.contracts.model;
+
+
+public interface DocumentHeaderRepository {
+ DocumentHeader getOne(Long id);
+
+ void save(DocumentHeader header);
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/content/ContentVersion.java b/src/main/java/io/legacyfighter/cabs/contracts/model/content/ContentVersion.java
new file mode 100644
index 0000000..f64c4a7
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/content/ContentVersion.java
@@ -0,0 +1,14 @@
+package io.legacyfighter.cabs.contracts.model.content;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class ContentVersion {
+ private String contentVersion;
+
+ protected ContentVersion(){}
+
+ public ContentVersion(String contentVersion){
+ this.contentVersion = contentVersion;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentContent.java b/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentContent.java
new file mode 100644
index 0000000..6fd434c
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentContent.java
@@ -0,0 +1,47 @@
+package io.legacyfighter.cabs.contracts.model.content;
+
+import javax.persistence.Embedded;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import java.util.UUID;
+
+@Entity
+public class DocumentContent {
+ @Id
+ @GeneratedValue
+ private UUID id;
+
+ private UUID previousId;
+
+ private String physicalContent; //some kind of reference to file, version control. In sour sample i will be a blob stored in DB:)
+
+ @Embedded
+ private ContentVersion contentVersion;//just a human readable descriptor
+
+ protected DocumentContent() {
+
+ }
+
+ public DocumentContent(UUID previousId, ContentVersion contentVersion, String physicalContent) {
+ this.previousId = previousId;
+ this.contentVersion = contentVersion;
+ this.physicalContent = physicalContent;
+ }
+
+ public DocumentContent(ContentVersion version, String physicalContent) {
+ this(null, version, physicalContent);
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public String getPhysicalContent() {
+ return physicalContent;
+ }
+
+ public ContentVersion getDocumentVersion() {
+ return contentVersion;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentContentRepository.java b/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentContentRepository.java
new file mode 100644
index 0000000..88a65f8
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentContentRepository.java
@@ -0,0 +1,9 @@
+package io.legacyfighter.cabs.contracts.model.content;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.UUID;
+
+public interface DocumentContentRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentNumber.java b/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentNumber.java
new file mode 100644
index 0000000..6fa2aff
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/content/DocumentNumber.java
@@ -0,0 +1,16 @@
+package io.legacyfighter.cabs.contracts.model.content;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class DocumentNumber {
+ private String number;
+
+ protected DocumentNumber(){
+
+ }
+
+ public DocumentNumber(String number){
+ this.number = number;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/ChangeCommand.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/ChangeCommand.java
new file mode 100644
index 0000000..5100a59
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/ChangeCommand.java
@@ -0,0 +1,38 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ChangeCommand {
+ private String desiredState;
+ private Map params;
+
+ public ChangeCommand(String desiredState, Map params){
+ this.desiredState = desiredState;
+ this.params = params;
+ }
+
+ public ChangeCommand(String desiredState){
+ this(desiredState, new HashMap<>());
+ }
+
+ public ChangeCommand withParam(String name, Object value){
+ params.put(name, value);
+ return this;
+ }
+
+ public String getDesiredState() {
+ return desiredState;
+ }
+
+ public T getParam(String name, Class type){
+ return (T) params.get(name);
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeCommand{" +
+ "desiredState='" + desiredState + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/State.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/State.java
new file mode 100644
index 0000000..a5b37c3
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/State.java
@@ -0,0 +1,145 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.contentchange.NegativePredicate;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange.PositiveVerifier;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+
+public class State {
+ //before: getClass().getName()
+ /**
+ * Unique name of a state
+ */
+ private final String stateDescriptor;
+
+ //TODO consider to get rid of this stateful object and transform State to reusable logic
+ private DocumentHeader documentHeader;
+
+
+ //TODO consider merging contentChangePredicate and afterContentChangeState int one function
+
+ //before: abstract canChangeContent()
+ /**
+ * predicates tested if content can be changed
+ */
+ private Predicate contentChangePredicate = new NegativePredicate(); //default
+
+ //before: abstract stateAfterContentChange()
+ /**
+ * state after content change - may be the same as before content change
+ */
+ private State afterContentChangeState;
+
+ //before: abstract canChangeFrom(state)
+ /**
+ * possible transitions to other states with rules that need to be tested to determine if transition is legal
+ */
+ private final Map>> stateChangePredicates = new HashMap<>();
+
+ //before: abstract acquire()
+ /**
+ * actions that may be needed to perform while transition to the next state
+ */
+ private final List> afterStateChangeActions = new ArrayList<>();
+
+ public State(String stateDescriptor){
+ this.stateDescriptor = stateDescriptor;
+ addStateChangePredicates(this, List.of(new PositiveVerifier()));//change to self is always possible
+ }
+
+ /**
+ * initial bounding with a document header
+ */
+ public void init(DocumentHeader documentHeader){
+ this.documentHeader = documentHeader;
+ documentHeader.setStateDescriptor(getStateDescriptor());
+ }
+
+ public State changeContent(ContentId currentContent){
+ if (!isContentEditable())
+ return this;
+
+ State newState = afterContentChangeState;//local variable just to focus attention
+ if (newState.contentChangePredicate.test(this)){
+ newState.init(documentHeader);
+ this.documentHeader.changeCurrentContent(currentContent);
+ return newState;
+ }
+
+ return this;
+ }
+
+
+ public State changeState(ChangeCommand command){
+ State desiredState = find(command.getDesiredState());
+ if (desiredState == null)
+ return this;
+
+ List> predicates = stateChangePredicates.getOrDefault(desiredState, Collections.emptyList());
+
+ if (predicates.stream().allMatch(e -> e.apply(this, command))) {
+ desiredState.init(documentHeader);
+ desiredState.afterStateChangeActions.forEach(e -> e.apply(documentHeader, command));
+ return desiredState;
+ }
+
+ return this;
+ }
+
+ public String getStateDescriptor(){
+ return stateDescriptor;
+ }
+
+ public DocumentHeader getDocumentHeader(){
+ return documentHeader;
+ }
+
+ public Map>> getStateChangePredicates() {
+ return stateChangePredicates;
+ }
+
+ public Predicate getContentChangePredicate() {
+ return contentChangePredicate;
+ }
+
+ public boolean isContentEditable(){
+ return afterContentChangeState != null;
+ }
+
+ @Override
+ public String toString() {
+ return "State{" +
+ "stateDescriptor='" + stateDescriptor + '\'' +
+ '}';
+ }
+
+ void addStateChangePredicates(State toState, List> predicatesToAdd) {
+ if (stateChangePredicates.containsKey(toState)) {
+ List> predicates = stateChangePredicates.get(toState);
+ predicates.addAll(predicatesToAdd);
+ }
+ else {
+ stateChangePredicates.put(toState, predicatesToAdd);
+ }
+ }
+
+ void addAfterStateChangeAction(BiFunction action) {
+ afterStateChangeActions.add(action);
+ }
+
+ void setAfterContentChangeState(State toState) {
+ afterContentChangeState = toState;
+ }
+
+ void setContentChangePredicate(Predicate predicate) {
+ contentChangePredicate = predicate;
+ }
+
+ private State find(String desiredState) {
+ return stateChangePredicates.keySet().stream().filter(e -> e.getStateDescriptor().equals(desiredState)).findFirst().orElse(null);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/StateBuilder.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/StateBuilder.java
new file mode 100644
index 0000000..23e7aac
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/StateBuilder.java
@@ -0,0 +1,139 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.contentchange.PositivePredicate;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange.PreviousStateVerifier;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+public class StateBuilder implements StateConfig{
+
+ //last step of the Builder - because it is special
+ public static class FinalStateConfig{
+ private final State state;
+
+ FinalStateConfig(State state){
+ this.state = state;
+ }
+
+ /**
+ * Adds an operation to be performed if state have changed
+ */
+ public FinalStateConfig action(BiFunction action) {
+ state.addAfterStateChangeAction(action);
+ return this;
+ }
+ }
+
+ /**
+ * This {@link StateBuilder} state, that depends on method call
+ */
+ private enum Mode{
+ /**
+ * Rules for state transition {@link #check(BiFunction) check} method called or {@link #from(String) from} method called
+ */
+ STATE_CHANGE,
+ /**
+ * Rules for content change {@link #whenContentChanged() whenContentChanged} method called
+ */
+ CONTENT_CHANGE
+ }
+ private Mode mode;
+
+ //all states configured so far
+ private Map states = new HashMap<>();
+
+ //below is the current state of the builder, gathered whit assembling methods, current state is reset in to() method
+ private State fromState;
+ private State initialState;
+ private List> predicates;
+
+ //========= methods for application layer - business process
+
+ @Override
+ public State begin(DocumentHeader header) {
+ header.setStateDescriptor(initialState.getStateDescriptor());
+ return recreate(header);
+ }
+
+ @Override
+ public State recreate(DocumentHeader header) {
+ State state = states.get(header.getStateDescriptor());
+ state.init(header);
+ return state;
+ }
+
+ //======= methods for assembling process
+
+ /**
+ * Similar to the {@link #from(String)} from} method, but marks initial state
+ */
+ public StateBuilder beginWith(String stateName) {
+ if (initialState != null)
+ throw new IllegalStateException("Initial state already set to: " + initialState.getStateDescriptor());
+
+ StateBuilder config = from(stateName);
+ initialState = fromState;
+ return config;
+ }
+
+ /**
+ * Begins a rule sequence with a beginning state
+ */
+ public StateBuilder from(String stateName) {
+ mode = Mode.STATE_CHANGE;
+ predicates = new ArrayList<>();
+ fromState = getOrPut(stateName);
+ return this;
+ }
+
+ /**
+ * Adds a rule to the current sequence
+ */
+ public StateBuilder check(BiFunction checkingFunction) {
+ mode = Mode.STATE_CHANGE;
+ predicates.add(checkingFunction);
+ return this;
+ }
+
+ /**
+ * Ends a rule sequence with a destination state
+ */
+ public FinalStateConfig to(String stateName) {
+ State toState = getOrPut(stateName);
+
+ switch (mode){
+ case STATE_CHANGE:
+ predicates.add(new PreviousStateVerifier(fromState.getStateDescriptor()));
+ fromState.addStateChangePredicates(toState, predicates);
+ break;
+ case CONTENT_CHANGE:
+ fromState.setAfterContentChangeState(toState);
+ toState.setContentChangePredicate(new PositivePredicate());
+ }
+
+ predicates = null;
+ fromState = null;
+ mode = null;
+
+ return new FinalStateConfig(toState);
+ }
+
+ /**
+ * Adds a rule of state change after a content change
+ */
+ public StateBuilder whenContentChanged() {
+ mode = Mode.CONTENT_CHANGE;
+ return this;
+ }
+
+ private State getOrPut(String stateName) {
+ if (!states.containsKey(stateName))
+ states.put(stateName, new State(stateName));
+ return states.get(stateName);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/StateConfig.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/StateConfig.java
new file mode 100644
index 0000000..4a11e0b
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/StateConfig.java
@@ -0,0 +1,9 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+
+public interface StateConfig {
+ State begin(DocumentHeader documentHeader);
+
+ State recreate(DocumentHeader documentHeader);
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/acme/AcmeContractStateAssembler.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/acme/AcmeContractStateAssembler.java
new file mode 100644
index 0000000..7ac32c7
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/acme/AcmeContractStateAssembler.java
@@ -0,0 +1,47 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.acme;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.StateBuilder;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.StateConfig;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.actions.ChangeVerifier;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.actions.PublishEvent;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.events.DocumentPublished;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.events.DocumentUnpublished;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange.AuthorIsNotAVerifier;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange.ContentNotEmptyVerifier;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+/**
+ * Sample static config.
+ */
+@Component
+public class AcmeContractStateAssembler {
+
+ public static final String VERIFIED = "verified";
+ public static final String DRAFT = "draft";
+ public static final String PUBLISHED = "published";
+ public static final String ARCHIVED = "archived";
+
+ public static final String PARAM_VERIFIER = ChangeVerifier.PARAM_VERIFIER;
+
+ private final ApplicationEventPublisher publisher;
+
+ @Autowired
+ public AcmeContractStateAssembler(ApplicationEventPublisher publisher) {
+ this.publisher = publisher;
+ }
+
+ public StateConfig assemble(){
+ StateBuilder builder = new StateBuilder();
+ builder.beginWith(DRAFT).check(new ContentNotEmptyVerifier()).check(new AuthorIsNotAVerifier()).to(VERIFIED).action(new ChangeVerifier());
+ builder.from(DRAFT).whenContentChanged().to(DRAFT);
+ //name of the "published" state and name of the DocumentPublished event are NOT correlated. These are two different domains, name similarity is just a coincidence
+ builder.from(VERIFIED).check(new ContentNotEmptyVerifier()).to(PUBLISHED).action(new PublishEvent(DocumentPublished.class, publisher));
+ builder.from(VERIFIED).whenContentChanged().to(DRAFT);
+ builder.from(DRAFT).to(ARCHIVED);
+ builder.from(VERIFIED).to(ARCHIVED);
+ builder.from(PUBLISHED).to(ARCHIVED).action(new PublishEvent(DocumentUnpublished.class, publisher));
+ return builder;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/actions/ChangeVerifier.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/actions/ChangeVerifier.java
new file mode 100644
index 0000000..bd9d665
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/actions/ChangeVerifier.java
@@ -0,0 +1,17 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.actions;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+
+import java.util.function.BiFunction;
+
+public class ChangeVerifier implements BiFunction {
+
+ public static final String PARAM_VERIFIER = "verifier";
+
+ @Override
+ public Void apply(DocumentHeader documentHeader, ChangeCommand command) {
+ documentHeader.setVerifierId(command.getParam(PARAM_VERIFIER, Long.class));
+ return null;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/actions/PublishEvent.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/actions/PublishEvent.java
new file mode 100644
index 0000000..49f3bf2
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/actions/PublishEvent.java
@@ -0,0 +1,38 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.actions;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.events.DocumentEvent;
+import org.springframework.context.ApplicationEventPublisher;
+
+import java.lang.reflect.Constructor;
+import java.util.function.BiFunction;
+
+public class PublishEvent implements BiFunction {
+
+ private Class extends DocumentEvent> eventClass;
+
+ private ApplicationEventPublisher publisher;
+
+ public PublishEvent(Class extends DocumentEvent> eventClass, ApplicationEventPublisher publisher) {
+ this.eventClass = eventClass;
+ this.publisher = publisher;
+ }
+
+ @Override
+ public Void apply(DocumentHeader documentHeader, ChangeCommand command) {
+ DocumentEvent event;
+ try {
+ Constructor extends DocumentEvent> constructor = eventClass.getDeclaredConstructor(Long.class, String.class, ContentId.class, DocumentNumber.class);
+ event = constructor.newInstance(documentHeader.getId(), documentHeader.getStateDescriptor(), documentHeader.getContentId(), documentHeader.getDocumentNumber());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ publisher.publishEvent(event);
+
+ return null;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/changes/FixedState.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/changes/FixedState.java
new file mode 100644
index 0000000..73c5962
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/changes/FixedState.java
@@ -0,0 +1,17 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.changes;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+
+import java.util.function.Function;
+
+public class FixedState implements Function {
+ private String stateName;
+ public FixedState(String stateName) {
+ this.stateName = stateName;
+ }
+
+ @Override
+ public State apply(State state) {
+ return new State(stateName);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentEvent.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentEvent.java
new file mode 100644
index 0000000..49ef203
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentEvent.java
@@ -0,0 +1,36 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.events;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import org.springframework.context.ApplicationEvent;
+
+public abstract class DocumentEvent extends ApplicationEvent {
+ private final Long documentId;
+ private final String currentSate;
+ private final ContentId contentId;
+ private final DocumentNumber number;
+
+ public DocumentEvent(Long documentId, String currentSate, ContentId contentId, DocumentNumber number) {
+ super(number);
+ this.documentId = documentId;
+ this.currentSate = currentSate;
+ this.contentId = contentId;
+ this.number = number;
+ }
+
+ public Long getDocumentId() {
+ return documentId;
+ }
+
+ public String getCurrentSate() {
+ return currentSate;
+ }
+
+ public ContentId getContentId() {
+ return contentId;
+ }
+
+ public DocumentNumber getNumber() {
+ return number;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentPublished.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentPublished.java
new file mode 100644
index 0000000..c419323
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentPublished.java
@@ -0,0 +1,10 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.events;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+
+public class DocumentPublished extends DocumentEvent{
+ public DocumentPublished(Long documentId, String currentSate, ContentId contentId, DocumentNumber number) {
+ super(documentId, currentSate, contentId, number);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentUnpublished.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentUnpublished.java
new file mode 100644
index 0000000..f5bf5e8
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/events/DocumentUnpublished.java
@@ -0,0 +1,10 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.events;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+
+public class DocumentUnpublished extends DocumentEvent{
+ public DocumentUnpublished(Long documentId, String currentSate, ContentId contentId, DocumentNumber number) {
+ super(documentId, currentSate, contentId, number);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/contentchange/NegativePredicate.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/contentchange/NegativePredicate.java
new file mode 100644
index 0000000..b079db0
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/contentchange/NegativePredicate.java
@@ -0,0 +1,12 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.contentchange;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+
+import java.util.function.Predicate;
+
+public class NegativePredicate implements Predicate {
+ @Override
+ public boolean test(State state) {
+ return false;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/contentchange/PositivePredicate.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/contentchange/PositivePredicate.java
new file mode 100644
index 0000000..ed94f29
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/contentchange/PositivePredicate.java
@@ -0,0 +1,12 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.contentchange;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+
+import java.util.function.Predicate;
+
+public class PositivePredicate implements Predicate {
+ @Override
+ public boolean test(State state) {
+ return true;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/AuthorIsNotAVerifier.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/AuthorIsNotAVerifier.java
new file mode 100644
index 0000000..4800bcd
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/AuthorIsNotAVerifier.java
@@ -0,0 +1,17 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.actions.ChangeVerifier;
+
+import java.util.function.BiFunction;
+
+public class AuthorIsNotAVerifier implements BiFunction {
+
+ public static final String PARAM_VERIFIER = ChangeVerifier.PARAM_VERIFIER;
+
+ @Override
+ public Boolean apply(State state, ChangeCommand command) {
+ return ! command.getParam(PARAM_VERIFIER, Long.class).equals(state.getDocumentHeader().getAuthorId());
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/ContentNotEmptyVerifier.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/ContentNotEmptyVerifier.java
new file mode 100644
index 0000000..c4259de
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/ContentNotEmptyVerifier.java
@@ -0,0 +1,14 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+
+import java.util.function.BiFunction;
+
+public class ContentNotEmptyVerifier implements BiFunction {
+
+ @Override
+ public Boolean apply(State state, ChangeCommand command) {
+ return state.getDocumentHeader().getContentId() != null;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/PositiveVerifier.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/PositiveVerifier.java
new file mode 100644
index 0000000..baeb295
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/PositiveVerifier.java
@@ -0,0 +1,14 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+
+import java.util.function.BiFunction;
+
+public class PositiveVerifier implements BiFunction {
+
+ @Override
+ public Boolean apply(State state, ChangeCommand command) {
+ return true;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/PreviousStateVerifier.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/PreviousStateVerifier.java
new file mode 100644
index 0000000..68bc5ef
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/dynamic/config/predicates/statechange/PreviousStateVerifier.java
@@ -0,0 +1,19 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.ChangeCommand;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.State;
+
+import java.util.function.BiFunction;
+
+public class PreviousStateVerifier implements BiFunction {
+ private final String stateDescriptor;
+
+ public PreviousStateVerifier(String stateDescriptor) {
+ this.stateDescriptor = stateDescriptor;
+ }
+
+ @Override
+ public Boolean apply(State state, ChangeCommand command) {
+ return state.getStateDescriptor().equals(stateDescriptor);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/BaseState.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/BaseState.java
new file mode 100644
index 0000000..456f52d
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/BaseState.java
@@ -0,0 +1,55 @@
+package io.legacyfighter.cabs.contracts.model.state.straightforward;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+
+//TODO introduce an interface
+
+public abstract class BaseState {
+ protected DocumentHeader documentHeader;
+
+ public void init(DocumentHeader documentHeader){
+ this.documentHeader = documentHeader;
+ documentHeader.setStateDescriptor(getStateDescriptor());
+ }
+
+ public BaseState changeContent(ContentId currentContent){
+ if (canChangeContent()){
+ BaseState newState = stateAfterContentChange();
+ newState.init(documentHeader);
+ this.documentHeader.changeCurrentContent(currentContent);
+ newState.acquire(documentHeader);
+ return newState;
+ }
+ return this;
+ }
+
+ protected abstract boolean canChangeContent();
+
+ protected abstract BaseState stateAfterContentChange();
+
+ public BaseState changeState(BaseState newState){
+ if (newState.canChangeFrom(this)) {
+ newState.init(documentHeader);
+ documentHeader.setStateDescriptor(newState.getStateDescriptor());
+ newState.acquire(documentHeader);
+ return newState;
+ }
+ return this;
+ }
+
+ public String getStateDescriptor(){
+ return getClass().getName();
+ }
+
+ /**
+ * template method that allows to perform addition actions during state change
+ */
+ protected abstract void acquire(DocumentHeader documentHeader);
+
+ protected abstract boolean canChangeFrom(BaseState previousState);
+
+ public DocumentHeader getDocumentHeader(){
+ return documentHeader;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/ArchivedState.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/ArchivedState.java
new file mode 100644
index 0000000..e0999dc
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/ArchivedState.java
@@ -0,0 +1,26 @@
+package io.legacyfighter.cabs.contracts.model.state.straightforward.acme;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+
+public class ArchivedState extends BaseState {
+ @Override
+ protected boolean canChangeContent() {
+ return false;
+ }
+
+ @Override
+ protected BaseState stateAfterContentChange() {
+ return this;
+ }
+
+ @Override
+ protected boolean canChangeFrom(BaseState previousState) {
+ return true;
+ }
+
+ @Override
+ protected void acquire(DocumentHeader documentHeader) {
+
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/DraftState.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/DraftState.java
new file mode 100644
index 0000000..4b49da4
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/DraftState.java
@@ -0,0 +1,34 @@
+package io.legacyfighter.cabs.contracts.model.state.straightforward.acme;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+
+public class DraftState extends BaseState {
+
+ //BAD IDEA!
+ //public BaseState publish(){
+ //if some validation
+ // return new PublishedState();
+ //}
+
+ @Override
+ protected boolean canChangeContent() {
+ return true;
+ }
+
+ @Override
+ protected BaseState stateAfterContentChange() {
+ return this;
+ }
+
+ @Override
+ protected boolean canChangeFrom(BaseState previousState) {
+ return true;
+ }
+
+ @Override
+ protected void acquire(DocumentHeader documentHeader) {
+
+ }
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/PublishedState.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/PublishedState.java
new file mode 100644
index 0000000..bb806eb
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/PublishedState.java
@@ -0,0 +1,28 @@
+package io.legacyfighter.cabs.contracts.model.state.straightforward.acme;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+
+public class PublishedState extends BaseState {
+
+ @Override
+ protected boolean canChangeContent() {
+ return false;
+ }
+
+ @Override
+ protected BaseState stateAfterContentChange() {
+ return this;
+ }
+
+ @Override
+ protected boolean canChangeFrom(BaseState previousState) {
+ return previousState instanceof VerifiedState
+ && previousState.getDocumentHeader().notEmpty();
+ }
+
+ @Override
+ protected void acquire(DocumentHeader documentHeader) {
+
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/VerifiedState.java b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/VerifiedState.java
new file mode 100644
index 0000000..f7adce6
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/contracts/model/state/straightforward/acme/VerifiedState.java
@@ -0,0 +1,40 @@
+package io.legacyfighter.cabs.contracts.model.state.straightforward.acme;
+
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+
+public class VerifiedState extends BaseState {
+ private Long verifierId;
+
+ public VerifiedState(Long verifierId) {
+ this.verifierId = verifierId;
+ }
+
+ public VerifiedState(){
+
+ }
+
+
+ @Override
+ protected boolean canChangeContent() {
+ return true;
+ }
+
+ @Override
+ protected BaseState stateAfterContentChange() {
+ return new DraftState();
+ }
+
+ @Override
+ protected boolean canChangeFrom(BaseState previousState) {
+ return previousState instanceof DraftState
+ && !previousState.getDocumentHeader().getAuthorId().equals(verifierId)
+ && previousState.getDocumentHeader().notEmpty();
+ }
+
+ @Override
+ protected void acquire(DocumentHeader documentHeader) {
+ documentHeader.setVerifierId(verifierId);
+ }
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/distance/Distance.java b/src/main/java/io/legacyfighter/cabs/distance/Distance.java
index b671629..0b5903a 100644
--- a/src/main/java/io/legacyfighter/cabs/distance/Distance.java
+++ b/src/main/java/io/legacyfighter/cabs/distance/Distance.java
@@ -1,26 +1,36 @@
package io.legacyfighter.cabs.distance;
+import javax.persistence.Embeddable;
import java.util.Locale;
import java.util.Objects;
+@Embeddable
public final class Distance {
public static final Distance ZERO = ofKm(0);
- private static final float MILES_TO_KILOMETERS_RATIO = 1.609344f;
+ private static final double MILES_TO_KILOMETERS_RATIO = 1.609344f;
- private final float km;
+ private double km;
public static Distance ofKm(float km) {
return new Distance(km);
}
- private Distance(float km) {
+ public static Distance ofKm(double km) {
+ return new Distance(km);
+ }
+
+ private Distance(double km) {
this.km = km;
}
public float toKmInFloat() {
- return km;
+ return (float) km;
+ }
+
+ private Distance() {
+
}
public String printIn(String unit) {
@@ -32,7 +42,7 @@ public String printIn(String unit) {
return String.format(Locale.US, "%.3f", km) + "km";
}
if (unit.equals("miles")) {
- float km = this.km / MILES_TO_KILOMETERS_RATIO;
+ double km = this.km / MILES_TO_KILOMETERS_RATIO;
if (km == Math.ceil(km)) {
return String.format(Locale.US, "%d", Math.round(km)) + "miles";
}
@@ -56,6 +66,21 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(km);
}
+
+ public double toKmInDouble() {
+ return km;
+ }
+
+ @Override
+ public String toString() {
+ return "Distance{" +
+ "km=" + km +
+ '}';
+ }
+
+ public Distance add(Distance travelled) {
+ return Distance.ofKm(this.km + travelled.km);
+ }
}
diff --git a/src/main/java/io/legacyfighter/cabs/driverreport/DriverReportController.java b/src/main/java/io/legacyfighter/cabs/driverreport/DriverReportController.java
new file mode 100644
index 0000000..863a12c
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/driverreport/DriverReportController.java
@@ -0,0 +1,25 @@
+package io.legacyfighter.cabs.driverreport;
+
+import io.legacyfighter.cabs.dto.*;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
+public class DriverReportController {
+
+ private final SqlBasedDriverReportCreator driverReportCreator;
+
+ DriverReportController(SqlBasedDriverReportCreator driverReportCreator) {
+ this.driverReportCreator = driverReportCreator;
+ }
+
+ @GetMapping("/driverreport/{driverId}")
+ @Transactional
+ public DriverReport loadReportForDriver(@PathVariable Long driverId, @RequestParam int lastDays) {
+ return driverReportCreator.createReport(driverId, lastDays);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/driverreport/SqlBasedDriverReportCreator.java b/src/main/java/io/legacyfighter/cabs/driverreport/SqlBasedDriverReportCreator.java
new file mode 100644
index 0000000..fe43798
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/driverreport/SqlBasedDriverReportCreator.java
@@ -0,0 +1,146 @@
+package io.legacyfighter.cabs.driverreport;
+
+import io.legacyfighter.cabs.distance.Distance;
+import io.legacyfighter.cabs.dto.*;
+import io.legacyfighter.cabs.entity.*;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Tuple;
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static io.legacyfighter.cabs.entity.DriverAttribute.DriverAttributeName.MEDICAL_EXAMINATION_REMARKS;
+
+@Service
+class SqlBasedDriverReportCreator {
+
+ private static final String QUERY_FOR_DRIVER_WITH_ATTRS =
+ "SELECT d.id, d.first_name, d.last_name, d.driver_license, " +
+ "d.photo, d.status, d.type, attr.name, attr.value " +
+ "FROM Driver d " +
+ "LEFT JOIN driver_attribute attr ON d.id = attr.driver_id " +
+ "WHERE d.id = :driverId AND attr.name <> :filteredAttr";
+
+ private static final String QUERY_FOR_SESSIONS = "SELECT ds.logged_at, ds.logged_out_at, ds.plates_number, ds.car_class, ds.car_brand, " +
+ "t.id as TRANSIT_ID, t.name as TARIFF_NAME, t.status as TRANSIT_STATUS, t.km, t.km_rate, " +
+ "t.price, t.drivers_fee, t.estimated_price, t.base_fee, " +
+ "t.date_time, t.published, t.accepted_at, t.started, t.complete_at, t.car_type, " +
+ "cl.id as CLAIM_ID, cl.owner_id, cl.reason, cl.incident_description, cl.status as CLAIM_STATUS, cl.creation_date, " +
+ "cl.completion_date, cl.change_date, cl.completion_mode, cl.claim_no, " +
+ "af.country as AF_COUNTRY, af.city as AF_CITY, af.street AS AF_STREET, af.building_number AS AF_NUMBER, " +
+ "ato.country as ATO_COUNTRY, ato.city as ATO_CITY, ato.street AS ATO_STREET, ato.building_number AS ATO_NUMBER, " +
+ "FROM driver_session ds " +
+ "LEFT JOIN transit t ON t.driver_id = ds.driver_id " +
+ "LEFT JOIN Address af ON t.from_id = af.id " +
+ "LEFT JOIN Address ato ON t.to_id = ato.id " +
+ "LEFT JOIN claim cl ON cl.transit_id = t.id " +
+ "WHERE ds.driver_id = :driverId AND t.status = :transitStatus " +
+ "AND ds.logged_at >= :since " +
+ "AND t.complete_at >= ds.logged_at " +
+ "AND t.complete_at <= ds.logged_out_at GROUP BY ds.id";
+
+ private final EntityManager em;
+
+ private final Clock clock;
+
+ SqlBasedDriverReportCreator(EntityManager entityManager, Clock clock) {
+ this.em = entityManager;
+ this.clock = clock;
+ }
+
+ DriverReport createReport(Long driverId, int lastDays) {
+ DriverReport driverReport = new DriverReport();
+ List driverInfo = em
+ .createNativeQuery(QUERY_FOR_DRIVER_WITH_ATTRS, Tuple.class)
+ .setParameter("driverId", driverId)
+ .setParameter("filteredAttr", MEDICAL_EXAMINATION_REMARKS.toString())
+ .getResultList();
+
+ driverInfo.forEach(tuple -> addAttrToReport(driverReport, tuple));
+ driverInfo.stream().findFirst().ifPresent(tuple -> addDriverToReport(driverReport, tuple));
+
+ Stream resultStream = em
+ .createNativeQuery(QUERY_FOR_SESSIONS, Tuple.class)
+ .setParameter("driverId", driverId)
+ .setParameter("transitStatus", Transit.Status.COMPLETED.ordinal())
+ .setParameter("since", calculateStartingPoint(lastDays))
+ .getResultStream();
+ Map> sessions = resultStream
+ .collect(
+ Collectors.toMap(
+ this::retrieveDrivingSession,
+ tuple -> List.of(
+ retrieveTransit(tuple)),
+ (existing, newOne) -> {
+ existing.addAll(newOne);
+ return existing;
+ }
+ ));
+
+ driverReport.setSessions(sessions);
+ return driverReport;
+ }
+
+ private TransitDTO retrieveTransit(Tuple tuple) {
+ return new TransitDTO(((Number) tuple.get("TRANSIT_ID")).longValue(), (String) tuple.get("TARIFF_NAME"),
+ Transit.Status.values()[((Integer) tuple.get("TRANSIT_STATUS"))], null,
+ Distance.ofKm(((Number) tuple.get("KM")).floatValue()), ((Number) tuple.get("KM_RATE")).floatValue(),
+ new BigDecimal(((Number) tuple.get("PRICE")).intValue()),
+ new BigDecimal(((Number) tuple.get("DRIVERS_FEE")).intValue()),
+ new BigDecimal(((Number) tuple.get("ESTIMATED_PRICE")).intValue()),
+ new BigDecimal(((Number) tuple.get("BASE_FEE")).intValue()),
+ ((Timestamp) tuple.get("DATE_TIME")).toInstant(), ((Timestamp) tuple.get("PUBLISHED")).toInstant(),
+ ((Timestamp) tuple.get("ACCEPTED_AT")).toInstant(), ((Timestamp) tuple.get("STARTED")).toInstant(),
+ ((Timestamp) tuple.get("COMPLETE_AT")).toInstant(), retrieveClaim(tuple), null, retrieveFromAddress(tuple), retrieveToAddress(tuple),
+ CarType.CarClass.valueOf((String) tuple.get("CAR_TYPE")), null);
+ }
+
+ private DriverSessionDTO retrieveDrivingSession(Tuple tuple) {
+ return new DriverSessionDTO(((Timestamp) tuple.get("LOGGED_AT")).toInstant(), ((Timestamp) tuple.get("LOGGED_OUT_AT")).toInstant(), (String) tuple.get("PLATES_NUMBER"), CarType.CarClass.valueOf((String) tuple.get("CAR_CLASS")), (String) tuple.get("CAR_BRAND"));
+ }
+
+ private AddressDTO retrieveToAddress(Tuple tuple) {
+ return new AddressDTO((String) tuple.get("AF_COUNTRY"), (String) tuple.get("AF_CITY"), (String) tuple.get("AF_STREET"), tuple.get("AF_NUMBER") == null ? null : ((Integer) tuple.get("AF_NUMBER")));
+ }
+
+ private AddressDTO retrieveFromAddress(Tuple tuple) {
+ return new AddressDTO((String) tuple.get("AF_COUNTRY"), (String) tuple.get("AF_CITY"), (String) tuple.get("AF_STREET"), tuple.get("AF_NUMBER") == null ? null : ((Integer) tuple.get("AF_NUMBER")));
+ }
+
+ private ClaimDTO retrieveClaim(Tuple tuple) {
+ Number claim_id = (Number) tuple.get("CLAIM_ID");
+ if (claim_id == null) {
+ return null;
+ }
+ return new ClaimDTO(claim_id.longValue(), ((Number) tuple.get("OWNER_ID")).longValue(), ((Number) tuple.get("TRANSIT_ID")).longValue(),
+ (String) tuple.get("REASON"), (String) tuple.get("INCIDENT_DESCRIPTION"), ((Timestamp) tuple.get("CREATION_DATE")).toInstant(),
+ tuple.get("COMPLETION_DATE") == null ? null : ((Timestamp) tuple.get("COMPLETION_DATE")).toInstant(),
+ tuple.get("CHANGE_DATE") == null ? null : ((Timestamp) tuple.get("CHANGE_DATE")).toInstant(), tuple.get("COMPLETION_MODE") == null ? null : Claim.CompletionMode.valueOf((String) tuple.get("COMPLETION_MODE")),
+ Claim.Status.valueOf((String) tuple.get("CLAIM_STATUS")), (String) tuple.get("CLAIM_NO"));
+ }
+
+ private Instant calculateStartingPoint(int lastDays) {
+ Instant beggingOfToday = Instant.now(clock).atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay().toInstant(OffsetDateTime.now().getOffset());
+ Instant since = beggingOfToday.minus(lastDays, ChronoUnit.DAYS);
+ return since;
+ }
+
+ private void addDriverToReport(DriverReport driverReport, Tuple tuple) {
+ Integer driverType = (Integer) tuple.get("TYPE");
+ driverReport.setDriverDTO(new DriverDTO(((Number) tuple.get("ID")).longValue(), (String) tuple.get("FIRST_NAME"), (String) tuple.get("LAST_NAME"), (String) tuple.get("DRIVER_LICENSE"), (String) tuple.get("PHOTO"), Driver.Status.values()[(Integer) tuple.get("STATUS")], driverType == null ? null : Driver.Type.values()[driverType]));
+ }
+
+ private void addAttrToReport(DriverReport driverReport, Tuple tuple) {
+ driverReport.addAttr(DriverAttribute.DriverAttributeName.valueOf((String) tuple.get("NAME")), (String) tuple.get(8));
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistance.java b/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistance.java
new file mode 100644
index 0000000..a8f8468
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistance.java
@@ -0,0 +1,150 @@
+package io.legacyfighter.cabs.driverreport.travelleddistance;
+
+import io.legacyfighter.cabs.distance.Distance;
+
+import javax.persistence.*;
+import java.time.*;
+import java.util.Objects;
+import java.util.UUID;
+
+import static java.lang.String.format;
+
+@Entity
+class TravelledDistance {
+
+ @Id
+ private UUID intervalId = UUID.randomUUID();
+
+ @Column(nullable = false)
+ private Long driverId;
+
+ @Column(nullable = false)
+ private TimeSlot timeSlot;
+
+ @Column(nullable = false)
+ private double lastLatitude;
+
+ @Column(nullable = false)
+ private double lastLongitude;
+
+ @Embedded
+ private Distance distance;
+
+ private TravelledDistance() {
+ }
+
+ TravelledDistance(Long driverId, TimeSlot timeSlot, double lastLatitude, double lastLongitude) {
+ this.driverId = driverId;
+ this.timeSlot = timeSlot;
+ this.lastLatitude = lastLatitude;
+ this.lastLongitude = lastLongitude;
+ this.distance = Distance.ZERO;
+ }
+
+ boolean contains(Instant timestamp) {
+ return timeSlot.contains(timestamp);
+ }
+
+ double getLastLongitude() {
+ return lastLongitude;
+ }
+
+ double getLastLatitude() {
+ return lastLatitude;
+ }
+
+ void addDistance(Distance travelled, double latitude, double longitude) {
+ this.distance = distance.add(travelled);
+ this.lastLatitude = latitude;
+ this.lastLongitude = longitude;
+ }
+
+ boolean endsAt(Instant instant) {
+ return timeSlot.endsAt(instant);
+ }
+
+ boolean isBefore(Instant now) {
+ return timeSlot.isBefore(now);
+ }
+}
+
+@Embeddable
+class TimeSlot {
+
+ static final int FIVE_MINUTES = 300;
+
+ @Column(nullable = false)
+ private Instant beginning;
+
+ @Column(nullable = false)
+ private Instant end;
+
+ private TimeSlot(Instant beginning, Instant end) {
+ this.beginning = beginning;
+ this.end = end;
+ }
+
+ static TimeSlot of(Instant beginning, Instant end) {
+ if (!end.isAfter(beginning)) {
+ throw new IllegalArgumentException(format("From %s is after to %s", beginning, end));
+ }
+ return new TimeSlot(beginning, end);
+ }
+
+ static TimeSlot slotThatContains(Instant seed) {
+ LocalDateTime startOfDay = seed.atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay();
+ LocalDateTime seedDateTime = seed.atZone(ZoneId.systemDefault()).toLocalDateTime();
+ long secondsFromStartOfDay = Duration.between(startOfDay, seedDateTime).toSeconds();
+ long intervals = (long) Math.floor((secondsFromStartOfDay / (double) FIVE_MINUTES));
+ Instant from = startOfDay.atZone(ZoneId.systemDefault()).plusSeconds(intervals * FIVE_MINUTES).toInstant();
+ return new TimeSlot(from, from.plusSeconds(FIVE_MINUTES));
+ }
+
+ private TimeSlot() {
+ }
+
+ boolean contains(Instant timestamp) {
+ return timestamp.isBefore(end) && !beginning.isAfter(timestamp);
+ }
+
+ boolean endsAt(Instant timestamp) {
+ return this.end.equals(timestamp);
+ }
+
+ boolean isBefore(Instant timestamp) {
+ return end.isBefore(timestamp);
+ }
+
+ TimeSlot prev() {
+ return new TimeSlot(beginning.minusSeconds(FIVE_MINUTES), end.minusSeconds(FIVE_MINUTES));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TimeSlot that = (TimeSlot) o;
+ return Objects.equals(beginning, that.beginning) && Objects.equals(end, that.end);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(beginning, end);
+ }
+
+ Instant beginning() {
+ return beginning;
+ }
+
+ Instant end() {
+ return end;
+ }
+
+ @Override
+ public String toString() {
+ return "Slot{" +
+ "beginning=" + beginning +
+ ", end=" + end +
+ '}';
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistanceRepository.java b/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistanceRepository.java
new file mode 100644
index 0000000..ecbf01d
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistanceRepository.java
@@ -0,0 +1,22 @@
+package io.legacyfighter.cabs.driverreport.travelleddistance;
+
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+
+import java.time.Instant;
+import java.util.UUID;
+
+interface TravelledDistanceRepository extends CrudRepository {
+
+ @Query("select td from TravelledDistance td where td.timeSlot.beginning <= :when and :when < td.timeSlot.end and td.driverId = :driverId")
+ TravelledDistance findTravelledDistanceTimeSlotByTime(Instant when, Long driverId);
+
+ TravelledDistance findTravelledDistanceByTimeSlotAndDriverId(TimeSlot timeSlot, Long driverId);
+
+ @Query(value = "SELECT COALESCE(SUM(_inner.km), 0) FROM " +
+ "( (SELECT * FROM travelled_distance td WHERE td.beginning >= :beginning AND td.driver_id = :driverId)) " +
+ "AS _inner WHERE end <= :to ", nativeQuery = true)
+ double calculateDistance(Instant beginning, Instant to, Long driverId);
+
+}
+
diff --git a/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistanceService.java b/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistanceService.java
new file mode 100644
index 0000000..1a419e6
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/driverreport/travelleddistance/TravelledDistanceService.java
@@ -0,0 +1,73 @@
+package io.legacyfighter.cabs.driverreport.travelleddistance;
+
+
+import io.legacyfighter.cabs.distance.Distance;
+import io.legacyfighter.cabs.service.DistanceCalculator;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Clock;
+import java.time.Instant;
+
+import static io.legacyfighter.cabs.driverreport.travelleddistance.TimeSlot.slotThatContains;
+
+@Service
+public class TravelledDistanceService {
+
+ private final Clock clock;
+ private final TravelledDistanceRepository travelledDistanceRepository;
+ private final DistanceCalculator distanceCalculator;
+
+ TravelledDistanceService(Clock clock, TravelledDistanceRepository travelledDistanceRepository, DistanceCalculator distanceCalculator) {
+ this.clock = clock;
+ this.travelledDistanceRepository = travelledDistanceRepository;
+ this.distanceCalculator = distanceCalculator;
+ }
+
+ public Distance calculateDistance(Long driverId, Instant from, Instant to) {
+ TimeSlot left = slotThatContains(from);
+ TimeSlot right = slotThatContains(to);
+ return Distance.ofKm(travelledDistanceRepository.calculateDistance(left.beginning(), right.end(), driverId));
+ }
+
+ @Transactional
+ public void addPosition(Long driverId, double latitude, double longitude, Instant seenAt) {
+ TravelledDistance matchedSlot = travelledDistanceRepository.findTravelledDistanceTimeSlotByTime(seenAt, driverId);
+ Instant now = clock.instant();
+ if (matchedSlot != null) {
+ if (matchedSlot.contains(now)) {
+ addDistanceToSlot(matchedSlot, latitude, longitude);
+ } else if (matchedSlot.isBefore(now)) {
+ recalculateDistanceFor(matchedSlot, driverId);
+ }
+ } else {
+ TimeSlot currentTimeSlot = slotThatContains(now);
+ TimeSlot prev = currentTimeSlot.prev();
+ TravelledDistance prevTravelledDistance = travelledDistanceRepository.findTravelledDistanceByTimeSlotAndDriverId(prev, driverId);
+ if (prevTravelledDistance != null) {
+ if (prevTravelledDistance.endsAt(seenAt)) {
+ addDistanceToSlot(prevTravelledDistance, latitude, longitude);
+ }
+ }
+ createSlotForNow(driverId, currentTimeSlot, latitude, longitude);
+ }
+ }
+
+ private void addDistanceToSlot(TravelledDistance aggregatedDistance, double latitude, double longitude) {
+ Distance travelled = Distance.ofKm(distanceCalculator.calculateByGeo(
+ latitude,
+ longitude,
+ aggregatedDistance.getLastLatitude(),
+ aggregatedDistance.getLastLongitude()));
+ aggregatedDistance.addDistance(travelled, latitude, longitude);
+ }
+
+ private void recalculateDistanceFor(TravelledDistance aggregatedDistance, Long driverId) {
+ //TODO
+ }
+
+ private void createSlotForNow(Long driverId, TimeSlot timeSlot, double latitude, double longitude) {
+ travelledDistanceRepository.save(new TravelledDistance(driverId, timeSlot, latitude, longitude));
+ }
+
+}
diff --git a/src/main/java/io/legacyfighter/cabs/dto/ClaimDTO.java b/src/main/java/io/legacyfighter/cabs/dto/ClaimDTO.java
index 385f259..1ac747e 100644
--- a/src/main/java/io/legacyfighter/cabs/dto/ClaimDTO.java
+++ b/src/main/java/io/legacyfighter/cabs/dto/ClaimDTO.java
@@ -28,25 +28,27 @@ public class ClaimDTO {
private Claim.Status status;
+ public ClaimDTO(Long claimID, Long clientId, Long transitId, String reason, String incidentDescription, Instant creationDate, Instant completionDate, Instant changeDate, Claim.CompletionMode completionMode, Claim.Status status, String claimNo) {
+ this.claimID = claimID;
+ this.clientId = clientId;
+ this.transitId = transitId;
+ this.reason = reason;
+ this.incidentDescription = incidentDescription;
+ this.isDraft = status.equals(Claim.Status.DRAFT);
+ this.creationDate = creationDate;
+ this.completionDate = completionDate;
+ this.changeDate = changeDate;
+ this.completionMode = completionMode;
+ this.status = status;
+ this.claimNo = claimNo;
+ }
+
private String claimNo;
public ClaimDTO(Claim claim) {
- if (claim.getStatus().equals(Claim.Status.DRAFT)) {
- this.setDraft(true);
- } else {
- this.setDraft(false);
- }
- this.setClaimID(claim.getId());
- this.setReason(claim.getReason());
- this.setIncidentDescription(claim.getIncidentDescription());
- this.setTransitId(claim.getTransit().getId());
- this.setClientId(claim.getOwner().getId());
- this.setCompletionDate(claim.getCompletionDate());
- this.setChangeDate(claim.getChangeDate());
- this.setClaimNo(claim.getClaimNo());
- this.setStatus(claim.getStatus());
- this.setCompletionMode(claim.getCompletionMode());
- this.setCreationDate(claim.getCreationDate());
+ this(claim.getId(), claim.getOwner().getId(), claim.getTransit().getId(), claim.getReason(),
+ claim.getIncidentDescription(), claim.getCreationDate(),
+ claim.getCompletionDate(), claim.getChangeDate(), claim.getCompletionMode(), claim.getStatus(), claim.getClaimNo());
}
public ClaimDTO() {
diff --git a/src/main/java/io/legacyfighter/cabs/dto/DriverDTO.java b/src/main/java/io/legacyfighter/cabs/dto/DriverDTO.java
index 71ba6dd..41a3022 100644
--- a/src/main/java/io/legacyfighter/cabs/dto/DriverDTO.java
+++ b/src/main/java/io/legacyfighter/cabs/dto/DriverDTO.java
@@ -17,6 +17,16 @@ public class DriverDTO {
private Driver.Type type;
+ public DriverDTO(Long id, String firstName, String lastName, String driverLicense, String photo, Driver.Status status, Driver.Type type) {
+ this.id = id;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.driverLicense = driverLicense;
+ this.photo = photo;
+ this.status = status;
+ this.type = type;
+ }
+
public DriverDTO(Driver driver) {
this.id = driver.getId();
firstName = driver.getFirstName();
diff --git a/src/main/java/io/legacyfighter/cabs/dto/DriverReport.java b/src/main/java/io/legacyfighter/cabs/dto/DriverReport.java
index 6ad5e16..c6f9e1a 100644
--- a/src/main/java/io/legacyfighter/cabs/dto/DriverReport.java
+++ b/src/main/java/io/legacyfighter/cabs/dto/DriverReport.java
@@ -1,6 +1,8 @@
package io.legacyfighter.cabs.dto;
+import io.legacyfighter.cabs.entity.DriverAttribute;
+
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -38,5 +40,8 @@ public void setSessions(Map> sessions) {
this.sessions = sessions;
}
+ public void addAttr(DriverAttribute.DriverAttributeName name, String value) {
+ attributes.add(new DriverAttributeDTO(name, value));
+ }
}
diff --git a/src/main/java/io/legacyfighter/cabs/dto/DriverSessionDTO.java b/src/main/java/io/legacyfighter/cabs/dto/DriverSessionDTO.java
index d5b5c37..c585cb1 100644
--- a/src/main/java/io/legacyfighter/cabs/dto/DriverSessionDTO.java
+++ b/src/main/java/io/legacyfighter/cabs/dto/DriverSessionDTO.java
@@ -18,6 +18,14 @@ public class DriverSessionDTO {
private String carBrand;
+ public DriverSessionDTO(Instant loggedAt, Instant loggedOutAt, String platesNumber, CarType.CarClass carClass, String carBrand) {
+ this.loggedAt = loggedAt;
+ this.loggedOutAt = loggedOutAt;
+ this.platesNumber = platesNumber;
+ this.carClass = carClass;
+ this.carBrand = carBrand;
+ }
+
public DriverSessionDTO() {
}
diff --git a/src/main/java/io/legacyfighter/cabs/dto/TransitDTO.java b/src/main/java/io/legacyfighter/cabs/dto/TransitDTO.java
index 90f00d6..32e786d 100644
--- a/src/main/java/io/legacyfighter/cabs/dto/TransitDTO.java
+++ b/src/main/java/io/legacyfighter/cabs/dto/TransitDTO.java
@@ -7,8 +7,6 @@
import java.math.BigDecimal;
import java.time.Instant;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
@@ -67,50 +65,57 @@ public TransitDTO() {
}
public TransitDTO(Transit transit) {
- id = transit.getId();
- distance = transit.getKm();
- factor = 1;
- if (transit.getPrice() != null) {
- price = new BigDecimal(transit.getPrice().toInt());
- }
- date = transit.getDateTime();
- status = transit.getStatus();
- setTariff(transit);
+ this(transit.getId(), transit.getTariff().getName(),
+ transit.getStatus(), transit.getDriver() == null ? null : new DriverDTO(transit.getDriver()),
+ transit.getKm(), transit.getTariff().getKmRate(),
+ transit.getPrice() != null ? new BigDecimal(transit.getPrice().toInt()) : null,
+ transit.getDriversFee() != null ? new BigDecimal(transit.getDriversFee().toInt()) : null,
+ transit.getEstimatedPrice() != null ? new BigDecimal(transit.getEstimatedPrice().toInt()) : null,
+ new BigDecimal(transit.getTariff().getBaseFee()),
+ transit.getDateTime(), transit.getPublished(),
+ transit.getAcceptedAt(), transit.getStarted(), transit.getCompleteAt(),
+ null, new ArrayList<>(), new AddressDTO(transit.getFrom()),
+ new AddressDTO(transit.getTo()), transit.getCarType(), new ClientDTO(transit.getClient()));
+
for (Driver d : transit.getProposedDrivers()) {
proposedDrivers.add(new DriverDTO(d));
}
- to = new AddressDTO(transit.getTo());
- from = new AddressDTO(transit.getFrom());
- carClass = transit.getCarType();
- clientDTO = new ClientDTO(transit.getClient());
- if (transit.getDriversFee() != null) {
- driverFee = new BigDecimal(transit.getDriversFee().toInt());
- }
- if (transit.getEstimatedPrice() != null) {
- estimatedPrice = new BigDecimal(transit.getEstimatedPrice().toInt());
- }
- dateTime = transit.getDateTime();
- published = transit.getPublished();
- acceptedAt = transit.getAcceptedAt();
- started = transit.getStarted();
- completeAt = transit.getCompleteAt();
+ }
+ public TransitDTO(Long id, String tariff, Transit.Status status, DriverDTO driver,
+ Distance distance, float kmRate, BigDecimal price, BigDecimal driverFee,
+ BigDecimal estimatedPrice, BigDecimal baseFee, Instant dateTime,
+ Instant published, Instant acceptedAt, Instant started, Instant completeAt,
+ ClaimDTO claimDTO, List proposedDrivers, AddressDTO from, AddressDTO to,
+ CarType.CarClass carClass, ClientDTO clientDTO) {
+ this.id = id;
+ this.factor = 1;
+ this.tariff = tariff;
+ this.status = status;
+ this.driver = driver;
+ this.distance = distance;
+ this.kmRate = kmRate;
+ this.price = price;
+ this.driverFee = driverFee;
+ this.estimatedPrice = estimatedPrice;
+ this.baseFee = baseFee;
+ this.dateTime = dateTime;
+ this.published = published;
+ this.acceptedAt = acceptedAt;
+ this.started = started;
+ this.completeAt = completeAt;
+ this.claimDTO = claimDTO;
+ this.proposedDrivers = proposedDrivers;
+ this.to = to;
+ this.from = from;
+ this.carClass = carClass;
+ this.clientDTO = clientDTO;
}
public float getKmRate() {
return kmRate;
}
- private void setTariff(Transit transit) {
- LocalDateTime day = date.atZone(ZoneId.systemDefault()).toLocalDateTime();
-
- // wprowadzenie nowych cennikow od 1.01.2019
- this.tariff = transit.getTariff().getName();
- this.kmRate = transit.getTariff().getKmRate();
- this.baseFee = new BigDecimal(transit.getTariff().getBaseFee());
-
- }
-
public String getTariff() {
return tariff;
}
diff --git a/src/main/java/io/legacyfighter/cabs/entity/DriverPosition.java b/src/main/java/io/legacyfighter/cabs/entity/DriverPosition.java
index b01a78f..4e44dfe 100644
--- a/src/main/java/io/legacyfighter/cabs/entity/DriverPosition.java
+++ b/src/main/java/io/legacyfighter/cabs/entity/DriverPosition.java
@@ -23,6 +23,13 @@ public class DriverPosition extends BaseEntity {
public DriverPosition() {
}
+ public DriverPosition(Driver driver, Instant seenAt, double latitude, double longitude) {
+ this.driver = driver;
+ this.seenAt = seenAt;
+ this.latitude = latitude;
+ this.longitude = longitude;
+ }
+
public Driver getDriver() {
return driver;
}
diff --git a/src/main/java/io/legacyfighter/cabs/money/Money.java b/src/main/java/io/legacyfighter/cabs/money/Money.java
index d0538f3..5bf6ef0 100644
--- a/src/main/java/io/legacyfighter/cabs/money/Money.java
+++ b/src/main/java/io/legacyfighter/cabs/money/Money.java
@@ -29,6 +29,10 @@ public Money percentage(int percentage) {
return new Money((int) Math.round(percentage * value/100.0));
}
+ public Money percentage(Double percentage) {
+ return new Money((int) Math.round(percentage * value/100));
+ }
+
public Integer toInt() {
return value;
}
diff --git a/src/main/java/io/legacyfighter/cabs/party/api/PartyId.java b/src/main/java/io/legacyfighter/cabs/party/api/PartyId.java
new file mode 100644
index 0000000..e8719d3
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/api/PartyId.java
@@ -0,0 +1,33 @@
+package io.legacyfighter.cabs.party.api;
+
+import java.util.Objects;
+import java.util.UUID;
+
+public class PartyId {
+ private UUID id;
+
+ public PartyId(){
+ this.id = UUID.randomUUID();
+ }
+
+ public PartyId(UUID id){
+ this.id = id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof PartyId)) return false;
+ PartyId baseId = (PartyId) o;
+ return id.equals(baseId.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ public UUID toUUID() {
+ return id;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/api/PartyMapper.java b/src/main/java/io/legacyfighter/cabs/party/api/PartyMapper.java
new file mode 100644
index 0000000..68b7e89
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/api/PartyMapper.java
@@ -0,0 +1,19 @@
+package io.legacyfighter.cabs.party.api;
+
+import io.legacyfighter.cabs.party.model.party.PartyRelationship;
+import io.legacyfighter.cabs.party.model.party.PartyRelationshipRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+public class PartyMapper {
+
+ @Autowired
+ private PartyRelationshipRepository partyRelationshipRepository;
+
+ public Optional mapRelation(PartyId id, String relationshipName) {
+ return partyRelationshipRepository.findRelationshipFor(id, relationshipName);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/api/RoleObjectFactory.java b/src/main/java/io/legacyfighter/cabs/party/api/RoleObjectFactory.java
new file mode 100644
index 0000000..679c208
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/api/RoleObjectFactory.java
@@ -0,0 +1,51 @@
+package io.legacyfighter.cabs.party.api;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.party.PartyRelationship;
+import io.legacyfighter.cabs.party.model.role.PartyBasedRole;
+import io.legacyfighter.cabs.party.utils.PolymorphicHashMap;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Sample impl based on Class-Instance map.
+ * More advanced impls can be case on a DI container: getRole can obtain role instance from the container.
+ *
+ * TODO introduce an interface to convert to Abstract Factory Pattern to be able to choose factory impl
+ */
+public class RoleObjectFactory {
+
+ Map, PartyBasedRole> roles = new PolymorphicHashMap<>();
+
+ public boolean hasRole(Class extends PartyBasedRole> role){
+ return roles.containsKey(role);
+ }
+
+ public static RoleObjectFactory from(PartyRelationship partyRelationship){
+ RoleObjectFactory roleObject = new RoleObjectFactory();
+ roleObject.add(partyRelationship);
+ return roleObject;
+ }
+
+ private void add(PartyRelationship partyRelationship){
+ add(partyRelationship.getRoleA(), partyRelationship.getPartyA());
+ add(partyRelationship.getRoleB(), partyRelationship.getPartyB());
+ }
+
+ private void add(String role, Party party) {
+ try {
+ //in sake of simplicity: a role name is same as a class name with no mapping between them
+ Class clazz = (Class) Class.forName(role);
+ PartyBasedRole instance = clazz.getConstructor(Party.class).newInstance(party);
+ roles.put(clazz, instance);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+
+ public Optional getRole(Class role){
+ return (Optional) Optional.ofNullable(roles.get(role));
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/infra/JpaPartyRelationshipRepository.java b/src/main/java/io/legacyfighter/cabs/party/infra/JpaPartyRelationshipRepository.java
new file mode 100644
index 0000000..66f5742
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/infra/JpaPartyRelationshipRepository.java
@@ -0,0 +1,67 @@
+package io.legacyfighter.cabs.party.infra;
+
+import io.legacyfighter.cabs.party.api.PartyId;
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.party.PartyRelationship;
+import io.legacyfighter.cabs.party.model.party.PartyRelationshipRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.EntityManager;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public class JpaPartyRelationshipRepository implements PartyRelationshipRepository {
+
+ @Autowired
+ private EntityManager entityManager;
+
+
+ @Override
+ public PartyRelationship put(String partyRelationship, String partyARole, Party partyA, String partyBRole, Party partyB){
+ List parties = entityManager.createQuery("SELECT r FROM PartyRelationship r " +
+ "WHERE r.name = :name " +
+ "AND (" +
+ "(r.partyA = :partyA AND r.partyB = :partyB) " +
+ "OR " +
+ "(r.partyA = : partyB AND r.partyB = :partyA)" +
+ ")")
+
+ .setParameter("name", partyRelationship)
+ .setParameter("partyA", partyA)
+ .setParameter("partyB", partyB)
+ .getResultList();
+ PartyRelationship relationship;
+
+ if (parties.size() == 0){
+ relationship = new PartyRelationship();
+ entityManager.persist(relationship);
+ }
+ else{
+ relationship = parties.get(0);
+ }
+
+ relationship.setName(partyRelationship);
+ relationship.setPartyA(partyA);
+ relationship.setPartyB(partyB);
+ relationship.setRoleA(partyARole);
+ relationship.setRoleB(partyBRole);
+
+ return relationship;
+ }
+
+ @Override
+ public Optional findRelationshipFor(PartyId id, String relationshipName) {
+ List parties = entityManager.createQuery("SELECT r FROM PartyRelationship r " +
+ "WHERE r.name = :name " +
+ "AND " +
+ "(r.partyA.id = :id OR r.partyB.id = :id)")
+ .setParameter("name", relationshipName)
+ .setParameter("id", id.toUUID())
+ .getResultList();
+ if (parties.size() == 0)
+ return Optional.empty();
+ return Optional.of(parties.get(0));
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/infra/JpaPartyRepository.java b/src/main/java/io/legacyfighter/cabs/party/infra/JpaPartyRepository.java
new file mode 100644
index 0000000..fdc67fb
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/infra/JpaPartyRepository.java
@@ -0,0 +1,27 @@
+package io.legacyfighter.cabs.party.infra;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.party.PartyRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.EntityManager;
+import java.util.UUID;
+
+@Repository
+public class JpaPartyRepository implements PartyRepository {
+
+ @Autowired
+ private EntityManager entityManager;
+
+ @Override
+ public Party put(UUID id) {
+ Party party = entityManager.find(Party.class, id);
+ if (party == null){
+ party = new Party();
+ party.setId(id);
+ entityManager.persist(party);
+ }
+ return party;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/model/party/Party.java b/src/main/java/io/legacyfighter/cabs/party/model/party/Party.java
new file mode 100644
index 0000000..a1e8554
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/model/party/Party.java
@@ -0,0 +1,19 @@
+package io.legacyfighter.cabs.party.model.party;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import java.util.UUID;
+
+@Entity
+public class Party {
+ @Id
+ UUID id;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRelationship.java b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRelationship.java
new file mode 100644
index 0000000..fa34483
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRelationship.java
@@ -0,0 +1,85 @@
+package io.legacyfighter.cabs.party.model.party;
+
+import javax.persistence.*;
+import java.util.Objects;
+import java.util.UUID;
+
+@Entity
+public class PartyRelationship {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private UUID id;
+
+ private String name;
+
+ private String roleA;//String in sake of simplicity, each domain will use own ENUM
+
+ private String roleB;//String in sake of simplicity, each domain will use own ENUM
+
+ @ManyToOne
+ private Party partyA;
+
+ @ManyToOne
+ private Party partyB;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getRoleA() {
+ return roleA;
+ }
+
+ public void setRoleA(String roleA) {
+ this.roleA = roleA;
+ }
+
+ public String getRoleB() {
+ return roleB;
+ }
+
+ public void setRoleB(String roleB) {
+ this.roleB = roleB;
+ }
+
+ public Party getPartyA() {
+ return partyA;
+ }
+
+ public void setPartyA(Party partyA) {
+ this.partyA = partyA;
+ }
+
+ public Party getPartyB() {
+ return partyB;
+ }
+
+ public void setPartyB(Party partyB) {
+ this.partyB = partyB;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ PartyRelationship that = (PartyRelationship) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRelationshipRepository.java b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRelationshipRepository.java
new file mode 100644
index 0000000..3bc732a
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRelationshipRepository.java
@@ -0,0 +1,10 @@
+package io.legacyfighter.cabs.party.model.party;
+
+import io.legacyfighter.cabs.party.api.PartyId;
+
+import java.util.Optional;
+
+public interface PartyRelationshipRepository {
+ PartyRelationship put(String partyRelationship, String partyARole, Party partyA, String partyBRole, Party partyB);
+ Optional findRelationshipFor(PartyId id, String relationshipName);
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRepository.java b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRepository.java
new file mode 100644
index 0000000..f9537f3
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRepository.java
@@ -0,0 +1,7 @@
+package io.legacyfighter.cabs.party.model.party;
+
+import java.util.UUID;
+
+public interface PartyRepository {
+ Party put(UUID id);
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRole.java b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRole.java
new file mode 100644
index 0000000..c30d31c
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/model/party/PartyRole.java
@@ -0,0 +1,32 @@
+package io.legacyfighter.cabs.party.model.party;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import java.util.UUID;
+
+@Entity
+public class PartyRole {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private UUID id;
+
+ private String name;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/model/role/PartyBasedRole.java b/src/main/java/io/legacyfighter/cabs/party/model/role/PartyBasedRole.java
new file mode 100644
index 0000000..c2bb73d
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/model/role/PartyBasedRole.java
@@ -0,0 +1,14 @@
+package io.legacyfighter.cabs.party.model.role;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+
+/**
+ * TODO introduce interface for an abstract class
+ */
+public abstract class PartyBasedRole {
+ protected Party party;
+
+ public PartyBasedRole(Party party){
+ this.party = party;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/party/utils/PolymorphicHashMap.java b/src/main/java/io/legacyfighter/cabs/party/utils/PolymorphicHashMap.java
new file mode 100644
index 0000000..ee03064
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/party/utils/PolymorphicHashMap.java
@@ -0,0 +1,24 @@
+package io.legacyfighter.cabs.party.utils;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+public class PolymorphicHashMap,V> extends HashMap {
+
+ @Override
+ public boolean containsKey(Object key) {
+ return findEntry((K)key).isPresent();
+ }
+
+ @Override
+ public V get(Object key) {
+ Optional> entry = findEntry((K)key);
+ return entry.map(Entry::getValue).orElse(null);
+ }
+
+ private Optional> findEntry(K key) {
+ return entrySet().stream()
+ .filter(e -> key.isAssignableFrom(e.getKey()))
+ .findFirst();
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/api/AssistanceRequest.java b/src/main/java/io/legacyfighter/cabs/repair/api/AssistanceRequest.java
new file mode 100644
index 0000000..330b1e6
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/api/AssistanceRequest.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.api;
+
+public class AssistanceRequest {
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/api/ContractManager.java b/src/main/java/io/legacyfighter/cabs/repair/api/ContractManager.java
new file mode 100644
index 0000000..c843929
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/api/ContractManager.java
@@ -0,0 +1,40 @@
+package io.legacyfighter.cabs.repair.api;
+
+import io.legacyfighter.cabs.party.api.PartyId;
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.party.PartyRelationshipRepository;
+import io.legacyfighter.cabs.party.model.party.PartyRepository;
+import io.legacyfighter.cabs.repair.model.dict.PartyRelationshipsDictionary;
+import io.legacyfighter.cabs.repair.model.dict.PartyRolesDictionary;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class ContractManager {
+
+ @Autowired
+ private PartyRepository partyRepository;
+
+ @Autowired
+ private PartyRelationshipRepository partyRelationshipRepository;
+
+ public void extendedWarrantyContractSigned(PartyId insurerId, PartyId vehicleId){
+ Party insurer = partyRepository.put(insurerId.toUUID());
+ Party vehicle = partyRepository.put(vehicleId.toUUID());
+
+ partyRelationshipRepository.put(PartyRelationshipsDictionary.REPAIR.toString(),
+ PartyRolesDictionary.INSURER.getRoleName(), insurer,
+ PartyRolesDictionary.INSURED.getRoleName(), vehicle);
+ }
+
+ public void manufacturerWarrantyRegistered(PartyId distributorId, PartyId vehicleId) {
+ Party distributor = partyRepository.put(distributorId.toUUID());
+ Party vehicle = partyRepository.put(vehicleId.toUUID());
+
+ partyRelationshipRepository.put(PartyRelationshipsDictionary.REPAIR.toString(),
+ PartyRolesDictionary.GUARANTOR.getRoleName(), distributor,
+ PartyRolesDictionary.CUSTOMER.getRoleName(), vehicle);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/api/RepairProcess.java b/src/main/java/io/legacyfighter/cabs/repair/api/RepairProcess.java
new file mode 100644
index 0000000..3fe98c2
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/api/RepairProcess.java
@@ -0,0 +1,48 @@
+package io.legacyfighter.cabs.repair.api;
+
+import io.legacyfighter.cabs.party.api.PartyMapper;
+import io.legacyfighter.cabs.party.api.RoleObjectFactory;
+import io.legacyfighter.cabs.party.model.party.PartyRelationship;
+import io.legacyfighter.cabs.repair.model.dict.PartyRelationshipsDictionary;
+import io.legacyfighter.cabs.repair.model.roles.repair.RepairingResult;
+import io.legacyfighter.cabs.repair.model.roles.repair.RoleForRepairer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+public class RepairProcess {
+
+ private final PartyMapper partyMapper;
+
+ @Autowired
+ public RepairProcess(PartyMapper partyMapper){
+ this.partyMapper = partyMapper;
+ }
+
+ public ResolveResult resolve(RepairRequest repairRequest) {
+ return partyMapper.mapRelation(repairRequest.getVehicle(), PartyRelationshipsDictionary.REPAIR.name())
+ .map(RoleObjectFactory::from)
+ .flatMap(rof -> rof.getRole(RoleForRepairer.class))
+ .map(role -> role.handle(repairRequest))
+ .map(repairingResult -> new ResolveResult(ResolveResult.Status.SUCCESS, repairingResult.getHandlingParty(), repairingResult.getTotalCost(), repairingResult.getHandledParts()))
+ .orElseGet(() -> new ResolveResult(ResolveResult.Status.ERROR));
+ }
+
+ public ResolveResult resolve_oldschool_version(RepairRequest repairRequest) {
+ //who is responsible for repairing the vehicle
+ Optional relationship = partyMapper.mapRelation(repairRequest.getVehicle(), PartyRelationshipsDictionary.REPAIR.name());
+ if (relationship.isPresent()){
+ RoleObjectFactory roleObjectFactory = RoleObjectFactory.from(relationship.get());
+ //dynamically assigned rules
+ Optional role = roleObjectFactory.getRole(RoleForRepairer.class);
+ if (role.isPresent()) {
+ //actual repair request handling
+ RepairingResult repairingResult = role.get().handle(repairRequest);
+ return new ResolveResult(ResolveResult.Status.SUCCESS, repairingResult.getHandlingParty(), repairingResult.getTotalCost(), repairingResult.getHandledParts());
+ }
+ }
+ return new ResolveResult(ResolveResult.Status.ERROR);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/api/RepairRequest.java b/src/main/java/io/legacyfighter/cabs/repair/api/RepairRequest.java
new file mode 100644
index 0000000..5406b79
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/api/RepairRequest.java
@@ -0,0 +1,25 @@
+package io.legacyfighter.cabs.repair.api;
+
+import io.legacyfighter.cabs.party.api.PartyId;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import java.util.Set;
+
+public class RepairRequest {
+
+ private PartyId vehicle;
+ private Set partsToRepair;
+
+ public RepairRequest(PartyId vehicle, Set parts) {
+ this.vehicle = vehicle;
+ this.partsToRepair = parts;
+ }
+
+ public Set getPartsToRepair() {
+ return partsToRepair;
+ }
+
+ public PartyId getVehicle() {
+ return vehicle;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/api/ResolveResult.java b/src/main/java/io/legacyfighter/cabs/repair/api/ResolveResult.java
new file mode 100644
index 0000000..f05883e
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/api/ResolveResult.java
@@ -0,0 +1,48 @@
+package io.legacyfighter.cabs.repair.api;
+
+import io.legacyfighter.cabs.money.Money;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import java.util.Set;
+import java.util.UUID;
+
+public class ResolveResult {
+
+
+ public enum Status {
+ SUCCESS, ERROR;
+ }
+
+ private UUID handlingParty;
+ private Money totalCost;
+ private Set acceptedParts;
+ private Status status;
+
+
+ public ResolveResult(Status status, UUID handlingParty, Money totalCost, Set acceptedParts) {
+ this.status = status;
+ this.handlingParty = handlingParty;
+ this.totalCost = totalCost;
+ this.acceptedParts = acceptedParts;
+ }
+
+ public ResolveResult(Status status) {
+ this.status = status;
+ }
+
+ public UUID getHandlingParty() {
+ return handlingParty;
+ }
+
+ public Money getTotalCost() {
+ return totalCost;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public Set getAcceptedParts() {
+ return acceptedParts;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/dao/UserDAO.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/dao/UserDAO.java
new file mode 100644
index 0000000..629f089
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/dao/UserDAO.java
@@ -0,0 +1,25 @@
+package io.legacyfighter.cabs.repair.legacy.dao;
+
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+import io.legacyfighter.cabs.repair.legacy.user.CommonBaseAbstractUser;
+import io.legacyfighter.cabs.repair.legacy.user.EmployeeDriverWithOwnCar;
+import io.legacyfighter.cabs.repair.legacy.user.SignedContract;
+import org.springframework.stereotype.Repository;
+
+import java.util.Set;
+
+/*
+ Fake impl that fakes graph query and determining CommonBaseAbstractUser type
+ */
+@Repository
+public class UserDAO {
+ public CommonBaseAbstractUser getOne(Long userId) {
+ SignedContract contract = new SignedContract();
+ contract.setCoveredParts(Set.of(Parts.values()));
+ contract.setCoverageRatio(100.0);
+
+ EmployeeDriverWithOwnCar user = new EmployeeDriverWithOwnCar();
+ user.setContract(contract);
+ return user;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/job/CommonBaseAbstractJob.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/CommonBaseAbstractJob.java
new file mode 100644
index 0000000..49069ae
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/CommonBaseAbstractJob.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.legacy.job;
+
+public abstract class CommonBaseAbstractJob {
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/job/JobResult.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/JobResult.java
new file mode 100644
index 0000000..8f9d6d7
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/JobResult.java
@@ -0,0 +1,29 @@
+package io.legacyfighter.cabs.repair.legacy.job;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JobResult {
+ public enum Decision{REDIRECTION, ACCEPTED, ERROR;}
+
+ private Decision decision;
+
+ private Map params = new HashMap<>();
+
+ public JobResult(Decision decision) {
+ this.decision = decision;
+ }
+
+ public JobResult addParam(String name, Object value){
+ params.put(name, value);
+ return this;
+ }
+
+ public Object getParam(String name) {
+ return params.get(name);
+ }
+
+ public Decision getDecision() {
+ return decision;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/job/MaintenanceJob.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/MaintenanceJob.java
new file mode 100644
index 0000000..36edda0
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/MaintenanceJob.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.legacy.job;
+
+public class MaintenanceJob extends CommonBaseAbstractJob{
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/job/RepairJob.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/RepairJob.java
new file mode 100644
index 0000000..2b04403
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/job/RepairJob.java
@@ -0,0 +1,27 @@
+package io.legacyfighter.cabs.repair.legacy.job;
+
+import io.legacyfighter.cabs.money.Money;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import java.util.Set;
+
+public class RepairJob extends CommonBaseAbstractJob{
+ private Set partsToRepair;
+ private Money estimatedValue;
+
+ public Money getEstimatedValue() {
+ return estimatedValue;
+ }
+
+ public void setEstimatedValue(Money estimatedValue) {
+ this.estimatedValue = estimatedValue;
+ }
+
+ public Set getPartsToRepair() {
+ return partsToRepair;
+ }
+
+ public void setPartsToRepair(Set partsToRepair) {
+ this.partsToRepair = partsToRepair;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/parts/Parts.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/parts/Parts.java
new file mode 100644
index 0000000..5a9859b
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/parts/Parts.java
@@ -0,0 +1,5 @@
+package io.legacyfighter.cabs.repair.legacy.parts;
+
+public enum Parts{
+ ENGINE, GEARBOX, SUSPENSION, PAINT
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/service/JobDoer.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/service/JobDoer.java
new file mode 100644
index 0000000..d28d00d
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/service/JobDoer.java
@@ -0,0 +1,29 @@
+package io.legacyfighter.cabs.repair.legacy.service;
+
+import io.legacyfighter.cabs.repair.legacy.dao.UserDAO;
+import io.legacyfighter.cabs.repair.legacy.job.CommonBaseAbstractJob;
+import io.legacyfighter.cabs.repair.legacy.job.JobResult;
+import io.legacyfighter.cabs.repair.legacy.user.CommonBaseAbstractUser;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class JobDoer {
+
+ private UserDAO userDAO;
+
+ public JobDoer(UserDAO userDAO){
+ this.userDAO = userDAO; //I'll inject test double some day because it makes total sense to me
+ }
+
+ public JobResult repair(Long userId, CommonBaseAbstractJob job){
+ CommonBaseAbstractUser user = userDAO.getOne(userId);
+ return user.doJob(job);
+ }
+
+ public JobResult repair2parallelModels(Long userId, CommonBaseAbstractJob job){
+ CommonBaseAbstractUser user = userDAO.getOne(userId);
+ return user.doJob(job);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/service/JobDoerParalellModels.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/service/JobDoerParalellModels.java
new file mode 100644
index 0000000..dc8ea35
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/service/JobDoerParalellModels.java
@@ -0,0 +1,68 @@
+package io.legacyfighter.cabs.repair.legacy.service;
+
+import io.legacyfighter.cabs.party.api.PartyId;
+import io.legacyfighter.cabs.repair.api.RepairProcess;
+import io.legacyfighter.cabs.repair.api.RepairRequest;
+import io.legacyfighter.cabs.repair.api.ResolveResult;
+import io.legacyfighter.cabs.repair.legacy.dao.UserDAO;
+import io.legacyfighter.cabs.repair.legacy.job.CommonBaseAbstractJob;
+import io.legacyfighter.cabs.repair.legacy.job.JobResult;
+import io.legacyfighter.cabs.repair.legacy.job.RepairJob;
+import io.legacyfighter.cabs.repair.legacy.user.CommonBaseAbstractUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@Transactional
+public class JobDoerParalellModels {
+
+ private UserDAO userDAO;
+
+ @Autowired
+ private RepairProcess repairProcess;
+
+ public JobDoerParalellModels(UserDAO userDAO){
+ this.userDAO = userDAO; //I'll inject a test double some day because it makes total sense to me
+ }
+
+ public JobResult repair(Long userId, CommonBaseAbstractJob job){
+ CommonBaseAbstractUser user = userDAO.getOne(userId);
+ return user.doJob(job);
+ }
+
+ public JobResult repair2parallelModels(Long userId, CommonBaseAbstractJob job){
+ //legacy model
+ CommonBaseAbstractUser user = userDAO.getOne(userId);
+ JobResult jobResult = user.doJob(job);
+
+ //new model
+ ResolveResult newResult = runParallelModel(userId, (RepairJob) job);
+
+ compare(newResult, jobResult);
+
+ return jobResult;
+ }
+
+ private ResolveResult runParallelModel(Long userId, RepairJob job) {
+ PartyId vehicle = findVehicleFor(userId);
+ RepairRequest repairRequest = new RepairRequest(vehicle, job.getPartsToRepair());
+ return repairProcess.resolve(repairRequest);
+ }
+
+ private PartyId findVehicleFor(Long userId) {
+ //TODO search in graph
+ return new PartyId();
+ }
+
+ private void compare(ResolveResult resolveResult, JobResult jobResult){
+ assert (resolveResult.getStatus().equals(ResolveResult.Status.SUCCESS)
+ &&
+ jobResult.getDecision().equals(JobResult.Decision.ACCEPTED))
+ ||
+ (resolveResult.getStatus().equals(ResolveResult.Status.ERROR)
+ && jobResult.getDecision().equals(JobResult.Decision.ERROR));
+
+ //TODO
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/CommonBaseAbstractUser.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/CommonBaseAbstractUser.java
new file mode 100644
index 0000000..520b9e5
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/CommonBaseAbstractUser.java
@@ -0,0 +1,32 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+import io.legacyfighter.cabs.common.BaseEntity;
+import io.legacyfighter.cabs.repair.legacy.job.CommonBaseAbstractJob;
+import io.legacyfighter.cabs.repair.legacy.job.JobResult;
+import io.legacyfighter.cabs.repair.legacy.job.MaintenanceJob;
+import io.legacyfighter.cabs.repair.legacy.job.RepairJob;
+
+public abstract class CommonBaseAbstractUser extends BaseEntity {
+ public JobResult doJob(CommonBaseAbstractJob job){
+ //poor man's pattern matching
+ if (job instanceof RepairJob){
+ return handle((RepairJob) job);
+ }
+ if (job instanceof MaintenanceJob){
+ return handle((MaintenanceJob) job);
+ }
+ return defaultHandler(job);
+ }
+
+ protected JobResult handle(RepairJob job) {
+ return defaultHandler(job);
+ }
+
+ protected JobResult handle(MaintenanceJob job) {
+ return defaultHandler(job);
+ }
+
+ protected JobResult defaultHandler(CommonBaseAbstractJob job){
+ throw new IllegalArgumentException(getClass().getName() + " can not handle " + job.getClass().getName());
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriver.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriver.java
new file mode 100644
index 0000000..ac5c17f
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriver.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+public class EmployeeDriver extends CommonBaseAbstractUser{
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriverWithLeasedCar.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriverWithLeasedCar.java
new file mode 100644
index 0000000..7354c30
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriverWithLeasedCar.java
@@ -0,0 +1,22 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+import io.legacyfighter.cabs.repair.legacy.job.JobResult;
+import io.legacyfighter.cabs.repair.legacy.job.RepairJob;
+
+public class EmployeeDriverWithLeasedCar extends EmployeeDriver{
+
+ private Long lasingCompanyId;
+
+ @Override
+ protected JobResult handle(RepairJob job) {
+ return new JobResult(JobResult.Decision.REDIRECTION).addParam("shouldHandleBy", lasingCompanyId);
+ }
+
+ public Long getLasingCompanyId() {
+ return lasingCompanyId;
+ }
+
+ public void setLasingCompanyId(Long lasingCompanyId) {
+ this.lasingCompanyId = lasingCompanyId;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriverWithOwnCar.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriverWithOwnCar.java
new file mode 100644
index 0000000..bffb116
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/EmployeeDriverWithOwnCar.java
@@ -0,0 +1,33 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+import io.legacyfighter.cabs.money.Money;
+import io.legacyfighter.cabs.repair.legacy.job.JobResult;
+import io.legacyfighter.cabs.repair.legacy.job.RepairJob;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import javax.persistence.Entity;
+import javax.persistence.OneToOne;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+public class EmployeeDriverWithOwnCar extends EmployeeDriver{
+
+ @OneToOne
+ private SignedContract contract;
+
+ @Override
+ protected JobResult handle(RepairJob job) {
+ Set acceptedParts = new HashSet<>(job.getPartsToRepair());
+ acceptedParts.retainAll(contract.getCoveredParts());
+
+ Money coveredCost = job.getEstimatedValue().percentage(contract.getCoverageRatio());
+ Money totalCost = job.getEstimatedValue().subtract(coveredCost);
+
+ return new JobResult(JobResult.Decision.ACCEPTED).addParam("totalCost", totalCost).addParam("acceptedParts", acceptedParts);
+ }
+
+ public void setContract(SignedContract contract) {
+ this.contract = contract;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SignedContract.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SignedContract.java
new file mode 100644
index 0000000..a4d6a4a
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SignedContract.java
@@ -0,0 +1,33 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+import io.legacyfighter.cabs.common.BaseEntity;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import java.util.Set;
+
+@Entity
+public class SignedContract extends BaseEntity {
+ @ElementCollection(fetch = FetchType.EAGER)
+ private Set coveredParts;
+
+ private Double coverageRatio;
+
+ public Double getCoverageRatio() {
+ return coverageRatio;
+ }
+
+ public void setCoverageRatio(Double coverageRatio) {
+ this.coverageRatio = coverageRatio;
+ }
+
+ public Set getCoveredParts() {
+ return coveredParts;
+ }
+
+ public void setCoveredParts(Set coveredParts) {
+ this.coveredParts = coveredParts;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorDriver.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorDriver.java
new file mode 100644
index 0000000..7c2836c
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorDriver.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+public class SubcontractorDriver extends CommonBaseAbstractUser{
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorDriverWithOwnCar.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorDriverWithOwnCar.java
new file mode 100644
index 0000000..38561fb
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorDriverWithOwnCar.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+public class SubcontractorDriverWithOwnCar extends SubcontractorDriver{
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorWithRentedCar.java b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorWithRentedCar.java
new file mode 100644
index 0000000..f4618d6
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/legacy/user/SubcontractorWithRentedCar.java
@@ -0,0 +1,4 @@
+package io.legacyfighter.cabs.repair.legacy.user;
+
+public class SubcontractorWithRentedCar {
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/dict/PartyRelationshipsDictionary.java b/src/main/java/io/legacyfighter/cabs/repair/model/dict/PartyRelationshipsDictionary.java
new file mode 100644
index 0000000..210606c
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/dict/PartyRelationshipsDictionary.java
@@ -0,0 +1,8 @@
+package io.legacyfighter.cabs.repair.model.dict;
+
+/*
+ Enum that emulates database dictionary
+ */
+public enum PartyRelationshipsDictionary{
+ REPAIR, SERVICE, CLEANING
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/dict/PartyRolesDictionary.java b/src/main/java/io/legacyfighter/cabs/repair/model/dict/PartyRolesDictionary.java
new file mode 100644
index 0000000..0615ca0
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/dict/PartyRolesDictionary.java
@@ -0,0 +1,24 @@
+package io.legacyfighter.cabs.repair.model.dict;
+
+import io.legacyfighter.cabs.party.model.role.PartyBasedRole;
+import io.legacyfighter.cabs.repair.model.roles.empty.Customer;
+import io.legacyfighter.cabs.repair.model.roles.empty.Insured;
+import io.legacyfighter.cabs.repair.model.roles.repair.ExtendedInsurance;
+import io.legacyfighter.cabs.repair.model.roles.repair.Warranty;
+
+/*
+ Enum that emulates database dictionary
+ */
+public enum PartyRolesDictionary {
+ INSURER(ExtendedInsurance.class), INSURED(Insured.class), GUARANTOR(Warranty.class), CUSTOMER(Customer.class);
+
+ private final String name;
+
+ PartyRolesDictionary(Class extends PartyBasedRole> clazz){
+ name = clazz.getName();
+ }
+
+ public String getRoleName(){
+ return name;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/assistance/RoleForAssistance.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/assistance/RoleForAssistance.java
new file mode 100644
index 0000000..ce0bdf0
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/assistance/RoleForAssistance.java
@@ -0,0 +1,16 @@
+package io.legacyfighter.cabs.repair.model.roles.assistance;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.role.PartyBasedRole;
+import io.legacyfighter.cabs.repair.api.AssistanceRequest;
+
+/**
+ * Base class for all commands that are able to handle @{@link AssistanceRequest}
+ */
+public abstract class RoleForAssistance extends PartyBasedRole {
+ public RoleForAssistance(Party party) {
+ super(party);
+ }
+
+ public abstract void handle(AssistanceRequest request);
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/empty/Customer.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/empty/Customer.java
new file mode 100644
index 0000000..d7b7e82
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/empty/Customer.java
@@ -0,0 +1,11 @@
+package io.legacyfighter.cabs.repair.model.roles.empty;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.role.PartyBasedRole;
+
+
+public class Customer extends PartyBasedRole {
+ public Customer(Party party) {
+ super(party);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/empty/Insured.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/empty/Insured.java
new file mode 100644
index 0000000..5eb9b18
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/empty/Insured.java
@@ -0,0 +1,10 @@
+package io.legacyfighter.cabs.repair.model.roles.empty;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.role.PartyBasedRole;
+
+public class Insured extends PartyBasedRole {
+ public Insured(Party party) {
+ super(party);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/ExtendedInsurance.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/ExtendedInsurance.java
new file mode 100644
index 0000000..e27cc09
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/ExtendedInsurance.java
@@ -0,0 +1,24 @@
+package io.legacyfighter.cabs.repair.model.roles.repair;
+
+import io.legacyfighter.cabs.money.Money;
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.repair.api.RepairRequest;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+public class ExtendedInsurance extends RoleForRepairer {
+
+ public ExtendedInsurance(Party party) {
+ super(party);
+ }
+
+ public RepairingResult handle(RepairRequest repairRequest) {
+ Set handledParts = new HashSet<>(repairRequest.getPartsToRepair());
+ handledParts.remove(Parts.PAINT);
+
+ return new RepairingResult(party.getId(), Money.ZERO, handledParts);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/RepairingResult.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/RepairingResult.java
new file mode 100644
index 0000000..5f5b3f1
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/RepairingResult.java
@@ -0,0 +1,31 @@
+package io.legacyfighter.cabs.repair.model.roles.repair;
+
+import io.legacyfighter.cabs.money.Money;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import java.util.Set;
+import java.util.UUID;
+
+public class RepairingResult {
+ private final UUID handlingParty;
+ private final Money totalCost;
+ private final Set handledParts;
+
+ public RepairingResult(UUID handlingParty, Money totalCost, Set handledParts) {
+ this.handlingParty = handlingParty;
+ this.totalCost = totalCost;
+ this.handledParts = handledParts;
+ }
+
+ public UUID getHandlingParty() {
+ return handlingParty;
+ }
+
+ public Money getTotalCost() {
+ return totalCost;
+ }
+
+ public Set getHandledParts() {
+ return handledParts;
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/RoleForRepairer.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/RoleForRepairer.java
new file mode 100644
index 0000000..be9e368
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/RoleForRepairer.java
@@ -0,0 +1,16 @@
+package io.legacyfighter.cabs.repair.model.roles.repair;
+
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.party.model.role.PartyBasedRole;
+import io.legacyfighter.cabs.repair.api.RepairRequest;
+
+/**
+ * Base class for all commands that are able to handle @{@link RepairRequest RepairRequest}
+ */
+public abstract class RoleForRepairer extends PartyBasedRole {
+ public RoleForRepairer(Party party) {
+ super(party);
+ }
+
+ public abstract RepairingResult handle(RepairRequest repairRequest);
+}
diff --git a/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/Warranty.java b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/Warranty.java
new file mode 100644
index 0000000..b10b397
--- /dev/null
+++ b/src/main/java/io/legacyfighter/cabs/repair/model/roles/repair/Warranty.java
@@ -0,0 +1,22 @@
+package io.legacyfighter.cabs.repair.model.roles.repair;
+
+import io.legacyfighter.cabs.money.Money;
+import io.legacyfighter.cabs.party.model.party.Party;
+import io.legacyfighter.cabs.repair.api.RepairRequest;
+import io.legacyfighter.cabs.repair.legacy.parts.Parts;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class Warranty extends RoleForRepairer {
+
+ public Warranty(Party party) {
+ super(party);
+ }
+
+ public RepairingResult handle(RepairRequest repairRequest) {
+ Set handledParts = new HashSet<>(repairRequest.getPartsToRepair());
+
+ return new RepairingResult(party.getId(), Money.ZERO, handledParts);
+ }
+}
diff --git a/src/main/java/io/legacyfighter/cabs/service/DriverTrackingService.java b/src/main/java/io/legacyfighter/cabs/service/DriverTrackingService.java
index 40eda1d..f8f9981 100644
--- a/src/main/java/io/legacyfighter/cabs/service/DriverTrackingService.java
+++ b/src/main/java/io/legacyfighter/cabs/service/DriverTrackingService.java
@@ -1,6 +1,7 @@
package io.legacyfighter.cabs.service;
-import io.legacyfighter.cabs.entity.Address;
+import io.legacyfighter.cabs.distance.Distance;
+import io.legacyfighter.cabs.driverreport.travelleddistance.TravelledDistanceService;
import io.legacyfighter.cabs.entity.Driver;
import io.legacyfighter.cabs.entity.DriverPosition;
import io.legacyfighter.cabs.repository.DriverPositionRepository;
@@ -9,10 +10,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.time.Clock;
import java.time.Instant;
-import java.util.List;
-import java.util.stream.Collectors;
@Service
public class DriverTrackingService {
@@ -23,13 +21,10 @@ public class DriverTrackingService {
private DriverRepository driverRepository;
@Autowired
- private DistanceCalculator distanceCalculator;
-
- @Autowired
- private Clock clock;
+ private TravelledDistanceService travelledDistanceService;
@Transactional
- public DriverPosition registerPosition(Long driverId, double latitude, double longitude) {
+ public DriverPosition registerPosition(Long driverId, double latitude, double longitude, Instant seenAt) {
Driver driver = driverRepository.getOne(driverId);
if (driver == null) {
throw new IllegalArgumentException("Driver does not exists, id = " + driverId);
@@ -39,35 +34,19 @@ public DriverPosition registerPosition(Long driverId, double latitude, double lo
}
DriverPosition position = new DriverPosition();
position.setDriver(driver);
- position.setSeenAt(Instant.now());
+ position.setSeenAt(seenAt);
position.setLatitude(latitude);
position.setLongitude(longitude);
- return positionRepository.save(position);
+ position = positionRepository.save(position);
+ travelledDistanceService.addPosition(driverId, latitude, longitude, seenAt);
+ return position;
}
- public double calculateTravelledDistance(Long driverId, Instant from, Instant to) {
+ public Distance calculateTravelledDistance(Long driverId, Instant from, Instant to) {
Driver driver = driverRepository.getOne(driverId);
if (driver == null) {
throw new IllegalArgumentException("Driver does not exists, id = " + driverId);
}
- List positions = positionRepository.findByDriverAndSeenAtBetweenOrderBySeenAtAsc(driver, from, to);
- double distanceTravelled = 0;
-
- if (positions.size() > 1) {
- DriverPosition previousPosition = positions.get(0);
-
- for (DriverPosition position : positions.stream().skip(1).collect(Collectors.toList())) {
- distanceTravelled += distanceCalculator.calculateByGeo(
- previousPosition.getLatitude(),
- previousPosition.getLongitude(),
- position.getLatitude(),
- position.getLongitude()
- );
-
- previousPosition = position;
- }
- }
-
- return distanceTravelled;
+ return travelledDistanceService.calculateDistance(driverId, from, to);
}
}
diff --git a/src/main/java/io/legacyfighter/cabs/ui/DriverReportController.java b/src/main/java/io/legacyfighter/cabs/ui/DriverReportController.java
deleted file mode 100644
index 1fc1958..0000000
--- a/src/main/java/io/legacyfighter/cabs/ui/DriverReportController.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package io.legacyfighter.cabs.ui;
-
-import io.legacyfighter.cabs.dto.*;
-import io.legacyfighter.cabs.entity.Claim;
-import io.legacyfighter.cabs.entity.Driver;
-import io.legacyfighter.cabs.entity.DriverSession;
-import io.legacyfighter.cabs.entity.Transit;
-import io.legacyfighter.cabs.repository.ClaimRepository;
-import io.legacyfighter.cabs.repository.DriverRepository;
-import io.legacyfighter.cabs.repository.DriverSessionRepository;
-import io.legacyfighter.cabs.service.DriverService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.time.*;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static io.legacyfighter.cabs.entity.DriverAttribute.DriverAttributeName.MEDICAL_EXAMINATION_REMARKS;
-
-@RestController
-public class DriverReportController {
-
- @Autowired
- private DriverService driverService;
-
- @Autowired
- private DriverRepository driverRepository;
-
- @Autowired
- private ClaimRepository claimRepository;
-
- @Autowired
- private DriverSessionRepository driverSessionRepository;
-
- @Autowired
- private Clock clock;
-
- @GetMapping("/driverreport/{driverId}")
- @Transactional
- public DriverReport loadReportForDriver(@PathVariable Long driverId, @RequestParam int lastDays) {
- DriverReport driverReport = new DriverReport();
- DriverDTO driverDTO = driverService.loadDriver(driverId);
- driverReport.setDriverDTO(driverDTO);
- Driver driver = driverRepository.getOne(driverId);
- driver
- .getAttributes()
- .stream()
- .filter(attr -> !attr.getName().equals(MEDICAL_EXAMINATION_REMARKS))
- .forEach(attr -> driverReport.getAttributes()
- .add(new DriverAttributeDTO(attr)));
- Instant beggingOfToday = Instant.now(clock).atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay().toInstant(ZoneOffset.UTC);
- Instant since = beggingOfToday.minus(lastDays, ChronoUnit.DAYS);
- List allByDriverAndLoggedAtAfter = driverSessionRepository.findAllByDriverAndLoggedAtAfter(driver, since);
- Map> sessionsWithTransits = new HashMap<>();
- for (DriverSession session : allByDriverAndLoggedAtAfter) {
- DriverSessionDTO dto = new DriverSessionDTO(session);
- List transitsInSession =
- driver.getTransits().stream()
- .filter(t -> t.getStatus().equals(Transit.Status.COMPLETED) && !t.getCompleteAt().isBefore(session.getLoggedAt()) && !t.getCompleteAt().isAfter(session.getLoggedOutAt())).collect(Collectors.toList());
-
- List transitsDtosInSession = new ArrayList<>();
- for (Transit t : transitsInSession) {
- TransitDTO transitDTO = new TransitDTO(t);
- List byOwnerAndTransit = claimRepository.findByOwnerAndTransit(t.getClient(), t);
- if (!byOwnerAndTransit.isEmpty()) {
- ClaimDTO claim = new ClaimDTO(byOwnerAndTransit.get(0));
- transitDTO.setClaimDTO(claim);
- }
- transitsDtosInSession.add(transitDTO);
- }
- sessionsWithTransits.put(dto, transitsDtosInSession);
- }
- driverReport.setSessions(sessionsWithTransits);
- return driverReport;
- }
-
-
-
-}
diff --git a/src/main/java/io/legacyfighter/cabs/ui/DriverTrackingController.java b/src/main/java/io/legacyfighter/cabs/ui/DriverTrackingController.java
index 5b02a4f..46351cd 100644
--- a/src/main/java/io/legacyfighter/cabs/ui/DriverTrackingController.java
+++ b/src/main/java/io/legacyfighter/cabs/ui/DriverTrackingController.java
@@ -16,13 +16,13 @@ public class DriverTrackingController {
@PostMapping("/driverPositions/")
ResponseEntity create(DriverPositionDTO driverPositionDTO) {
- DriverPosition driverPosition = trackingService.registerPosition(driverPositionDTO.getDriverId(), driverPositionDTO.getLatitude(), driverPositionDTO.getLongitude());
+ DriverPosition driverPosition = trackingService.registerPosition(driverPositionDTO.getDriverId(), driverPositionDTO.getLatitude(), driverPositionDTO.getLongitude(), driverPositionDTO.getSeenAt());
return ResponseEntity.ok(toDto(driverPosition));
}
@GetMapping("/driverPositions/{id}/total")
double calculateTravelledDistance(@PathVariable Long id, @RequestParam Instant from, @RequestParam Instant to) {
- return trackingService.calculateTravelledDistance(id, from, to);
+ return trackingService.calculateTravelledDistance(id, from, to).toKmInDouble();
}
diff --git a/src/test/java/io/legacyfighter/cabs/common/Fixtures.java b/src/test/java/io/legacyfighter/cabs/common/Fixtures.java
index 4b562e1..8f79a0d 100644
--- a/src/test/java/io/legacyfighter/cabs/common/Fixtures.java
+++ b/src/test/java/io/legacyfighter/cabs/common/Fixtures.java
@@ -7,7 +7,6 @@
import io.legacyfighter.cabs.entity.Driver.Status;
import io.legacyfighter.cabs.money.Money;
import io.legacyfighter.cabs.repository.*;
-
import io.legacyfighter.cabs.service.AwardsService;
import io.legacyfighter.cabs.service.CarTypeService;
import io.legacyfighter.cabs.service.ClaimService;
@@ -17,7 +16,7 @@
import java.time.Instant;
import java.time.LocalDateTime;
-import java.time.ZoneOffset;
+import java.time.OffsetDateTime;
import java.util.stream.IntStream;
import static java.util.stream.IntStream.range;
@@ -50,6 +49,9 @@ public class Fixtures {
@Autowired
AwardsService awardsService;
+ @Autowired
+ DriverAttributeRepository driverAttributeRepository;
+
public Client aClient() {
return clientRepository.save(new Client());
}
@@ -61,7 +63,7 @@ public Client aClient(Client.Type type) {
}
public Transit aTransit(Driver driver, Integer price, LocalDateTime when, Client client) {
- Transit transit = new Transit(null, null, client, null, when.toInstant(ZoneOffset.UTC), Distance.ZERO);
+ Transit transit = new Transit(null, null, client, null, when.toInstant(OffsetDateTime.now().getOffset()), Distance.ZERO);
transit.setPrice(new Money(price));
transit.proposeTo(driver);
transit.acceptBy(driver, Instant.now());
@@ -94,7 +96,11 @@ public DriverFee driverHasFee(Driver driver, DriverFee.FeeType feeType, int amou
}
public Driver aDriver() {
- return driverService.createDriver("FARME100165AB5EW", "Kowalsi", "Janusz", Driver.Type.REGULAR, Status.ACTIVE, "");
+ return aDriver(Status.ACTIVE, "Janusz", "Kowalsi", "FARME100165AB5EW");
+ }
+
+ public Driver aDriver(Status status, String name, String lastName, String driverLicense) {
+ return driverService.createDriver(driverLicense, lastName, name, Driver.Type.REGULAR, status, "");
}
public Transit aCompletedTransitAt(int price, Instant when) {
@@ -154,6 +160,12 @@ public Claim createClaim(Client client, Transit transit) {
return claim;
}
+ public Claim createClaim(Client client, Transit transit, String reason) {
+ ClaimDTO claimDTO = claimDto("Okradli mnie na hajs", reason, client.getId(), transit.getId());
+ claimDTO.setDraft(false);
+ return claimService.create(claimDTO);
+ }
+
public Claim createAndResolveClaim(Client client, Transit transit) {
Claim claim = createClaim(client, transit);
claim = claimService.tryToResolveAutomatically(claim.getId());
@@ -188,4 +200,8 @@ public void activeAwardsAccount(Client client) {
awardsAccount(client);
awardsService.activateAccount(client.getId());
}
+
+ public void driverHasAttribute(Driver driver, DriverAttribute.DriverAttributeName name, String value) {
+ driverAttributeRepository.save(new DriverAttribute(driver, name, value));
+ }
}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/application/dynamic/AcmeContractManagerBasedOnDynamicStateModelTest.java b/src/test/java/io/legacyfighter/cabs/contracts/application/dynamic/AcmeContractManagerBasedOnDynamicStateModelTest.java
new file mode 100644
index 0000000..c45ca29
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/application/dynamic/AcmeContractManagerBasedOnDynamicStateModelTest.java
@@ -0,0 +1,141 @@
+package io.legacyfighter.cabs.contracts.application.dynamic;
+
+import io.legacyfighter.cabs.contracts.application.acme.dynamic.DocumentOperationResult;
+import io.legacyfighter.cabs.contracts.application.acme.dynamic.DocumentResourceManager;
+import io.legacyfighter.cabs.contracts.application.editor.CommitResult;
+import io.legacyfighter.cabs.contracts.application.editor.DocumentDTO;
+import io.legacyfighter.cabs.contracts.application.editor.DocumentEditor;
+import io.legacyfighter.cabs.contracts.legacy.User;
+import io.legacyfighter.cabs.contracts.legacy.UserRepository;
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.content.ContentVersion;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.acme.AcmeContractStateAssembler;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.predicates.statechange.AuthorIsNotAVerifier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest
+public class AcmeContractManagerBasedOnDynamicStateModelTest {
+
+ @Autowired
+ DocumentEditor editor;
+ @Autowired
+ DocumentResourceManager documentResourceManager;
+ @Autowired
+ UserRepository userRepository;
+
+ private static final String CONTENT_1 = "content 1";
+ private static final String CONTENT_2 = "content 2";
+ private static final ContentVersion ANY_VERSION = new ContentVersion("v1");
+
+ private User author;
+ private User verifier;
+
+ DocumentNumber documentNumber;
+ Long headerId;
+
+ @BeforeEach
+ public void prepare(){
+ author = userRepository.save(new User());
+ verifier = userRepository.save(new User());
+ }
+
+ @Test
+ public void verifierOtherThanAuthorCanVerify(){
+ //given
+ crateAcmeContract(author);
+ ContentId contentId = commitContent(CONTENT_1);
+ DocumentOperationResult result = documentResourceManager.changeContent(headerId, contentId);
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.DRAFT).editable().possibleNextStates(AcmeContractStateAssembler.VERIFIED,AcmeContractStateAssembler.ARCHIVED);
+ //when
+ result = documentResourceManager.changeState(headerId, AcmeContractStateAssembler.VERIFIED, verifierParam());
+ //then
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.VERIFIED).editable().possibleNextStates(AcmeContractStateAssembler.PUBLISHED, AcmeContractStateAssembler.ARCHIVED);
+ }
+
+
+
+ @Test
+ public void authorCanNotVerify(){
+ //given
+ crateAcmeContract(author);
+ ContentId contentId = commitContent(CONTENT_1);
+ DocumentOperationResult result = documentResourceManager.changeContent(headerId, contentId);
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.DRAFT);
+ //when
+ result = documentResourceManager.changeState(headerId, AcmeContractStateAssembler.VERIFIED, authorParam());
+ //then
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.DRAFT);
+ }
+
+ @Test
+ public void changingContentOfVerifiedMovesBackToDraft(){
+ //given
+ crateAcmeContract(author);
+ ContentId contentId = commitContent(CONTENT_1);
+ DocumentOperationResult result = documentResourceManager.changeContent(headerId, contentId);
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.DRAFT).editable();
+
+ result = documentResourceManager.changeState(headerId, AcmeContractStateAssembler.VERIFIED, verifierParam());
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.VERIFIED).editable();
+ //when
+ contentId = commitContent(CONTENT_2);
+ result = documentResourceManager.changeContent(headerId, contentId);
+ //then
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.DRAFT).editable();
+ }
+
+ @Test
+ public void publishedCanNotBeChanged(){
+ //given
+ crateAcmeContract(author);
+ ContentId firstContentId = commitContent(CONTENT_1);
+ DocumentOperationResult result = documentResourceManager.changeContent(headerId, firstContentId);
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.DRAFT).editable();
+
+ result = documentResourceManager.changeState(headerId, AcmeContractStateAssembler.VERIFIED, verifierParam());
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.VERIFIED).editable();
+
+ result = documentResourceManager.changeState(headerId, AcmeContractStateAssembler.PUBLISHED, emptyParam());
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.PUBLISHED).uneditable();
+ //when
+ ContentId newContentId = commitContent(CONTENT_2);
+ result = documentResourceManager.changeContent(headerId, newContentId);
+ //then
+ new DocumentOperationResultAssert(result).state(AcmeContractStateAssembler.PUBLISHED).uneditable().content(firstContentId);
+ }
+
+
+
+ private ContentId commitContent(String content) {
+ DocumentDTO doc = new DocumentDTO(null, content, ANY_VERSION);
+ CommitResult result = editor.commit(doc);
+ assertEquals(CommitResult.Result.SUCCESS, result.getResult());
+ return new ContentId(result.getContentId());
+ }
+
+ private void crateAcmeContract(User user){
+ DocumentOperationResult result = documentResourceManager.createDocument(user.getId());
+ documentNumber = result.getDocumentNumber();
+ headerId = result.getDocumentHeaderId();
+ }
+
+ private Map verifierParam() {
+ return Map.of(AuthorIsNotAVerifier.PARAM_VERIFIER, verifier.getId());
+ }
+
+ private Map authorParam() {
+ return Map.of(AuthorIsNotAVerifier.PARAM_VERIFIER, author.getId());
+ }
+
+ private Map emptyParam() {
+ return Map.of();
+ }
+}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/application/dynamic/DocumentOperationResultAssert.java b/src/test/java/io/legacyfighter/cabs/contracts/application/dynamic/DocumentOperationResultAssert.java
new file mode 100644
index 0000000..ff34765
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/application/dynamic/DocumentOperationResultAssert.java
@@ -0,0 +1,44 @@
+package io.legacyfighter.cabs.contracts.application.dynamic;
+
+import io.legacyfighter.cabs.contracts.application.acme.dynamic.DocumentOperationResult;
+import io.legacyfighter.cabs.contracts.model.ContentId;
+
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+public class DocumentOperationResultAssert {
+ private DocumentOperationResult result;
+
+ public DocumentOperationResultAssert(DocumentOperationResult result){
+ this.result = result;
+ assertEquals(DocumentOperationResult.Result.SUCCESS, result.getResult());
+ }
+
+ public DocumentOperationResultAssert editable(){
+ assertTrue(result.isContentChangePossible());
+ return this;
+ }
+
+ public DocumentOperationResultAssert uneditable(){
+ assertFalse(result.isContentChangePossible());
+ return this;
+ }
+
+ public DocumentOperationResultAssert state(String state){
+ assertEquals(state, result.getStateName());
+ return this;
+ }
+
+ public DocumentOperationResultAssert content(ContentId contentId) {
+ assertEquals(contentId, result.getContentId());
+ return this;
+ }
+
+ public DocumentOperationResultAssert possibleNextStates(String ... states){
+ assertEquals(Set.of(states),
+ result.getPossibleTransitionsAndRules().keySet());
+ return this;
+ }
+}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/application/straightforward/acme/AcmeContractProcessBasedOnStraightforwardStateModelTest.java b/src/test/java/io/legacyfighter/cabs/contracts/application/straightforward/acme/AcmeContractProcessBasedOnStraightforwardStateModelTest.java
new file mode 100644
index 0000000..507a7c6
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/application/straightforward/acme/AcmeContractProcessBasedOnStraightforwardStateModelTest.java
@@ -0,0 +1,101 @@
+package io.legacyfighter.cabs.contracts.application.straightforward.acme;
+
+import io.legacyfighter.cabs.contracts.application.acme.straigthforward.AcmeContractProcessBasedOnStraightforwardDocumentModel;
+import io.legacyfighter.cabs.contracts.application.acme.straigthforward.ContractResult;
+import io.legacyfighter.cabs.contracts.application.editor.CommitResult;
+import io.legacyfighter.cabs.contracts.application.editor.DocumentDTO;
+import io.legacyfighter.cabs.contracts.application.editor.DocumentEditor;
+import io.legacyfighter.cabs.contracts.legacy.User;
+import io.legacyfighter.cabs.contracts.legacy.UserRepository;
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.content.ContentVersion;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.acme.DraftState;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.acme.VerifiedState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@SpringBootTest
+public class AcmeContractProcessBasedOnStraightforwardStateModelTest {
+
+ @Autowired
+ DocumentEditor editor;
+ @Autowired
+ AcmeContractProcessBasedOnStraightforwardDocumentModel contractProcess;
+ @Autowired
+ UserRepository userRepository;
+
+ private static final String CONTENT_1 = "content 1";
+ private static final String CONTENT_2 = "content 2";
+ private static final ContentVersion ANY_VERSION = new ContentVersion("v1");
+
+ private User author;
+ private User verifier;
+
+ DocumentNumber documentNumber;
+ Long headerId;
+
+ @BeforeEach
+ public void prepare(){
+ author = userRepository.save(new User());
+ verifier = userRepository.save(new User());
+ }
+
+ @Test
+ public void verifierOtherThanAuthorCanVerify(){
+ //given
+ crateAcmeContract(author);
+ ContentId contentId = commitContent(CONTENT_1);
+ contractProcess.changeContent(headerId, contentId);
+ //when
+ ContractResult result = contractProcess.verify(headerId, verifier.getId());
+ //then
+ new ContractResultAssert(result).state(new VerifiedState(verifier.getId()));
+ }
+
+ @Test
+ public void authorCanNotVerify(){
+ //given
+ crateAcmeContract(author);
+ ContentId contentId = commitContent(CONTENT_1);
+ contractProcess.changeContent(headerId, contentId);
+ //when
+ ContractResult result = contractProcess.verify(headerId, author.getId());
+ //then
+ new ContractResultAssert(result).state(new DraftState());
+ }
+
+ @Test
+ public void changingContentOfVerifiedMovesBackToDraft(){
+ //given
+ crateAcmeContract(author);
+ ContentId contentId = commitContent(CONTENT_1);
+ ContractResult result = contractProcess.changeContent(headerId, contentId);
+ new ContractResultAssert(result).state(new DraftState());
+
+ result = contractProcess.verify(headerId, verifier.getId());
+ new ContractResultAssert(result).state(new VerifiedState(verifier.getId()));
+ //when
+ contentId = commitContent(CONTENT_2);
+ //then
+ result = contractProcess.changeContent(headerId, contentId);
+ new ContractResultAssert(result).state(new DraftState());
+ }
+
+ private ContentId commitContent(String content) {
+ DocumentDTO doc = new DocumentDTO(null, content, ANY_VERSION);
+ CommitResult result = editor.commit(doc);
+ assertEquals(CommitResult.Result.SUCCESS, result.getResult());
+ return new ContentId(result.getContentId());
+ }
+
+ private void crateAcmeContract(User user){
+ ContractResult contractResult = contractProcess.createContract(user.getId());
+ documentNumber = contractResult.getDocumentNumber();
+ headerId = contractResult.getDocumentHeaderId();
+ }
+}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/application/straightforward/acme/ContractResultAssert.java b/src/test/java/io/legacyfighter/cabs/contracts/application/straightforward/acme/ContractResultAssert.java
new file mode 100644
index 0000000..2c8916d
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/application/straightforward/acme/ContractResultAssert.java
@@ -0,0 +1,21 @@
+package io.legacyfighter.cabs.contracts.application.straightforward.acme;
+
+import io.legacyfighter.cabs.contracts.application.acme.straigthforward.ContractResult;
+import io.legacyfighter.cabs.contracts.model.state.straightforward.BaseState;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+public class ContractResultAssert {
+ private ContractResult result;
+
+ public ContractResultAssert(ContractResult result){
+ this.result = result;
+ assertEquals(ContractResult.Result.SUCCESS, result.getResult());
+ }
+
+ public ContractResultAssert state(BaseState state){
+ assertEquals(state.getStateDescriptor(), result.getStateDescriptor());
+ return this;
+ }
+}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/legacy/DocumentTest.java b/src/test/java/io/legacyfighter/cabs/contracts/legacy/DocumentTest.java
new file mode 100644
index 0000000..eb53bd6
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/legacy/DocumentTest.java
@@ -0,0 +1,51 @@
+package io.legacyfighter.cabs.contracts.legacy;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+public class DocumentTest {
+
+ private static final String ANY_NUMBER = "number";
+ private static final User ANY_USER = new User();
+ private static final User OTHER_USER = new User();
+ private static final String TITLE = "title";
+
+ @Test
+ public void onlyDraftCanBeVerifiedByUserOtherThanCreator(){
+ Document doc = new Document(ANY_NUMBER, ANY_USER);
+
+ doc.verifyBy(OTHER_USER);
+
+ assertEquals(DocumentStatus.VERIFIED, doc.getStatus());
+ }
+
+ @Test
+ public void canNotChangePublished(){
+ Document doc = new Document(ANY_NUMBER, ANY_USER);
+ doc.changeTitle(TITLE);
+ doc.verifyBy(OTHER_USER);
+ doc.publish();
+
+ try {
+ doc.changeTitle("");
+ }
+ catch (IllegalStateException ex){
+ assertTrue(true);
+ }
+ assertEquals(TITLE, doc.getTitle());
+ }
+
+ @Test
+ public void changingVerifiedMovesToDraft(){
+ Document doc = new Document(ANY_NUMBER, ANY_USER);
+ doc.changeTitle(TITLE);
+ doc.verifyBy(OTHER_USER);
+
+ doc.changeTitle("");
+
+ assertEquals(DocumentStatus.DRAFT, doc.getStatus());
+ }
+}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/model/state/dynamic/AcmeContractTest.java b/src/test/java/io/legacyfighter/cabs/contracts/model/state/dynamic/AcmeContractTest.java
new file mode 100644
index 0000000..b0ec63e
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/model/state/dynamic/AcmeContractTest.java
@@ -0,0 +1,100 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic;
+
+import io.legacyfighter.cabs.contracts.model.ContentId;
+import io.legacyfighter.cabs.contracts.model.DocumentHeader;
+import io.legacyfighter.cabs.contracts.model.content.DocumentNumber;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.acme.AcmeContractStateAssembler;
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.events.DocumentPublished;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+public class AcmeContractTest {
+
+ private static final DocumentNumber ANY_NUMBER = new DocumentNumber("nr: 1");
+ private static final Long ANY_USER = 1L;
+ private static final Long OTHER_USER = 2L;
+ private static final ContentId ANY_VERSION = new ContentId(UUID.randomUUID());
+ private static final ContentId OTHER_VERSION = new ContentId(UUID.randomUUID());
+
+ private FakeDocumentPublisher publisher;
+
+ State draft(){
+ DocumentHeader header = new DocumentHeader(ANY_USER, ANY_NUMBER);
+ header.setStateDescriptor(AcmeContractStateAssembler.DRAFT);
+ publisher = new FakeDocumentPublisher();
+
+ AcmeContractStateAssembler assembler = new AcmeContractStateAssembler(publisher);
+ StateConfig config = assembler.assemble();
+ State state = config.recreate(header);
+
+ return state;
+ }
+
+ @Test
+ public void draftCanBeVerifiedByUserOtherThanCreator(){
+ //given
+ State state = draft().changeContent(ANY_VERSION);
+ //when
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.VERIFIED).withParam(AcmeContractStateAssembler.PARAM_VERIFIER, OTHER_USER));
+ //then
+ assertEquals(AcmeContractStateAssembler.VERIFIED, state.getStateDescriptor());
+ assertEquals(OTHER_USER, state.getDocumentHeader().getVerifier());
+ }
+
+ @Test
+ public void canNotChangePublished(){
+ //given
+ State state = state = draft().changeContent(ANY_VERSION)
+ .changeState(new ChangeCommand(AcmeContractStateAssembler.VERIFIED).withParam(AcmeContractStateAssembler.PARAM_VERIFIER, OTHER_USER))
+ .changeState(new ChangeCommand(AcmeContractStateAssembler.PUBLISHED));
+
+ publisher.contains(DocumentPublished.class);
+ publisher.reset();
+ //when
+ state = state.changeContent(OTHER_VERSION);
+ //then
+ publisher.noEvents();
+ assertEquals(AcmeContractStateAssembler.PUBLISHED, state.getStateDescriptor());
+ assertEquals(ANY_VERSION, state.getDocumentHeader().getContentId());
+ }
+
+ @Test
+ public void changingVerifiedMovesToDraft(){
+ //given
+ State state = draft().changeContent(ANY_VERSION)
+ .changeState(new ChangeCommand(AcmeContractStateAssembler.VERIFIED).withParam(AcmeContractStateAssembler.PARAM_VERIFIER, OTHER_USER));
+ //when
+ state = state.changeContent(OTHER_VERSION);
+ //then
+ assertEquals(AcmeContractStateAssembler.DRAFT, state.getStateDescriptor());
+ assertEquals(OTHER_VERSION, state.getDocumentHeader().getContentId());
+ }
+
+
+ @Test
+ public void canChangeStateToTheSame(){
+ State state = draft().changeContent(ANY_VERSION);
+ assertEquals(AcmeContractStateAssembler.DRAFT, state.getStateDescriptor());
+ state.changeState(new ChangeCommand(AcmeContractStateAssembler.DRAFT));
+ assertEquals(AcmeContractStateAssembler.DRAFT, state.getStateDescriptor());
+
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.VERIFIED).withParam(AcmeContractStateAssembler.PARAM_VERIFIER, OTHER_USER));
+ assertEquals(AcmeContractStateAssembler.VERIFIED, state.getStateDescriptor());
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.VERIFIED).withParam(AcmeContractStateAssembler.PARAM_VERIFIER, OTHER_USER));
+ assertEquals(AcmeContractStateAssembler.VERIFIED, state.getStateDescriptor());
+
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.PUBLISHED));
+ assertEquals(AcmeContractStateAssembler.PUBLISHED, state.getStateDescriptor());
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.PUBLISHED));
+ assertEquals(AcmeContractStateAssembler.PUBLISHED, state.getStateDescriptor());
+
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.ARCHIVED));
+ assertEquals(AcmeContractStateAssembler.ARCHIVED, state.getStateDescriptor());
+ state = state.changeState(new ChangeCommand(AcmeContractStateAssembler.ARCHIVED));
+ assertEquals(AcmeContractStateAssembler.ARCHIVED, state.getStateDescriptor());
+ }
+}
diff --git a/src/test/java/io/legacyfighter/cabs/contracts/model/state/dynamic/FakeDocumentPublisher.java b/src/test/java/io/legacyfighter/cabs/contracts/model/state/dynamic/FakeDocumentPublisher.java
new file mode 100644
index 0000000..46ca7e6
--- /dev/null
+++ b/src/test/java/io/legacyfighter/cabs/contracts/model/state/dynamic/FakeDocumentPublisher.java
@@ -0,0 +1,36 @@
+package io.legacyfighter.cabs.contracts.model.state.dynamic;
+
+import io.legacyfighter.cabs.contracts.model.state.dynamic.config.events.DocumentEvent;
+import org.springframework.context.ApplicationEventPublisher;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+public class FakeDocumentPublisher implements ApplicationEventPublisher {
+
+ private Set