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 clazz = (Class) 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 eventClass; + + private ApplicationEventPublisher publisher; + + public PublishEvent(Class eventClass, ApplicationEventPublisher publisher) { + this.eventClass = eventClass; + this.publisher = publisher; + } + + @Override + public Void apply(DocumentHeader documentHeader, ChangeCommand command) { + DocumentEvent event; + try { + Constructor 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 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 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 events = new HashSet(); + + @Override + public void publishEvent(Object event) { + events.add(event); + } + + public void contains(Class event){ + boolean found = events.stream().anyMatch(e -> e.getClass().equals(event)); + assertTrue(found); + } + + public void noEvents() { + assertEquals(0, events.size()); + } + + public void reset() { + events.clear(); + } + + +} diff --git a/src/test/java/io/legacyfighter/cabs/contracts/model/state/straightforward/AcmeContractTest.java b/src/test/java/io/legacyfighter/cabs/contracts/model/state/straightforward/AcmeContractTest.java new file mode 100644 index 0000000..694d835 --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/contracts/model/state/straightforward/AcmeContractTest.java @@ -0,0 +1,67 @@ +package io.legacyfighter.cabs.contracts.model.state.straightforward; + +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.straightforward.acme.DraftState; +import io.legacyfighter.cabs.contracts.model.state.straightforward.acme.PublishedState; +import io.legacyfighter.cabs.contracts.model.state.straightforward.acme.VerifiedState; +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 BaseState state; + + @Test + public void onlyDraftCanBeVerifiedByUserOtherThanCreator(){ + //given + state = draft().changeContent(ANY_VERSION); + //when + state = state.changeState(new VerifiedState(OTHER_USER)); + //then + assertEquals(VerifiedState.class, state.getClass()); + assertEquals(OTHER_USER, state.getDocumentHeader().getVerifier()); + } + + @Test + public void canNotChangePublished(){ + //given + state = draft().changeContent(ANY_VERSION).changeState(new VerifiedState(OTHER_USER)).changeState(new PublishedState()); + //when + state = state.changeContent(OTHER_VERSION); + //then + assertEquals(PublishedState.class, state.getClass()); + assertEquals(ANY_VERSION, state.getDocumentHeader().getContentId()); + } + + @Test + public void changingVerifiedMovesToDraft(){ + //given + state = draft().changeContent(ANY_VERSION); + //when + state = state.changeState(new VerifiedState(OTHER_USER)).changeContent(OTHER_VERSION); + //then + assertEquals(DraftState.class, state.getClass()); + assertEquals(OTHER_VERSION, state.getDocumentHeader().getContentId()); + } + + private BaseState draft(){ + DocumentHeader header = new DocumentHeader(ANY_USER, ANY_NUMBER); + + BaseState state = new DraftState(); + state.init(header); + + return state; + } +} diff --git a/src/test/java/io/legacyfighter/cabs/distance/DistanceTest.java b/src/test/java/io/legacyfighter/cabs/distance/DistanceTest.java index c211eb6..dcff946 100644 --- a/src/test/java/io/legacyfighter/cabs/distance/DistanceTest.java +++ b/src/test/java/io/legacyfighter/cabs/distance/DistanceTest.java @@ -1,5 +1,6 @@ package io.legacyfighter.cabs.distance; +import io.legacyfighter.cabs.money.Money; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -22,6 +23,15 @@ void canConvertToFloat() { assertEquals(2f, Distance.ofKm(2).toKmInFloat()); } + @Test + void canConvertToDouble() { + //expect + assertEquals(2000d, Distance.ofKm(2000).toKmInDouble()); + assertEquals(0d, Distance.ofKm(0).toKmInDouble()); + assertEquals(312.22d, Distance.ofKm(312.22d).toKmInDouble()); + assertEquals(2d, Distance.ofKm(2).toKmInDouble()); + } + @Test void canRepresentDistanceAsMeters() { //expect @@ -51,6 +61,15 @@ void canRepresentDistanceAsMiles() { assertEquals("1.243miles", Distance.ofKm(2).printIn("miles")); } + @Test + void canAddDistances() { + //expect + assertEquals(Distance.ofKm(1000f), Distance.ofKm(500f).add(Distance.ofKm(500f))); + assertEquals(Distance.ofKm(1042f), Distance.ofKm(1020f).add(Distance.ofKm(22f))); + assertEquals(Distance.ofKm(0f), Distance.ofKm(0f).add(Distance.ofKm(0f))); + assertEquals(Distance.ofKm(3.7f), Distance.ofKm(1.5f).add(Distance.ofKm(2.2f))); + } + } \ No newline at end of file diff --git a/src/test/java/io/legacyfighter/cabs/driverreport/travelleddistance/SlotTest.java b/src/test/java/io/legacyfighter/cabs/driverreport/travelleddistance/SlotTest.java new file mode 100644 index 0000000..657b64c --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/driverreport/travelleddistance/SlotTest.java @@ -0,0 +1,103 @@ +package io.legacyfighter.cabs.driverreport.travelleddistance; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; + +import static io.legacyfighter.cabs.driverreport.travelleddistance.TimeSlot.slotThatContains; +import static java.time.temporal.ChronoUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.*; + +class SlotTest { + + static Instant NOON = LocalDateTime.of(1989, 12, 12, 12, 10).toInstant(OffsetDateTime.now().getOffset()); + static Instant NOON_FIVE = NOON.plus(5, MINUTES); + static final Instant NOON_TEN = NOON_FIVE.plus(5, MINUTES); + + @Test + void beginningMustBeBeforeEnd() { + //expect + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> TimeSlot.of(NOON_FIVE, NOON)); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> TimeSlot.of(NOON_TEN, NOON)); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> TimeSlot.of(NOON_TEN, NOON_FIVE)); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> TimeSlot.of(NOON_TEN, NOON_TEN)); + } + + @Test + void canCreateValidSlot() { + //given + TimeSlot noonToFive = TimeSlot.of(NOON, NOON_FIVE); + TimeSlot fiveToTen = TimeSlot.of(NOON_FIVE, NOON_TEN); + + //expect + assertEquals(NOON, noonToFive.beginning()); + assertEquals(NOON_FIVE, noonToFive.end()); + assertEquals(NOON_FIVE, fiveToTen.beginning()); + assertEquals(NOON_TEN, fiveToTen.end()); + } + + @Test + void canCreatePreviousSLot() { + //given + TimeSlot noonToFive = TimeSlot.of(NOON, NOON_FIVE); + TimeSlot fiveToTen = TimeSlot.of(NOON_FIVE, NOON_TEN); + TimeSlot tenToFifteen = TimeSlot.of(NOON_TEN, NOON_TEN.plus(5, MINUTES)); + + //expect + assertEquals(noonToFive, fiveToTen.prev()); + assertEquals(fiveToTen, tenToFifteen.prev()); + assertEquals(noonToFive, tenToFifteen.prev().prev()); + } + + @Test + void canCalculateIfTimestampIsWithin() { + //given + TimeSlot noonToFive = TimeSlot.of(NOON, NOON_FIVE); + TimeSlot fiveToTen = TimeSlot.of(NOON_FIVE, NOON_TEN); + + //expect + assertTrue(noonToFive.contains(NOON)); + assertTrue(noonToFive.contains(NOON.plus(1, MINUTES))); + assertFalse(noonToFive.contains(NOON_FIVE)); + assertFalse(noonToFive.contains(NOON_FIVE.plus(1, MINUTES))); + + assertFalse(noonToFive.isBefore(NOON)); + assertFalse(noonToFive.isBefore(NOON_FIVE)); + assertTrue(noonToFive.isBefore(NOON_TEN)); + + assertTrue(noonToFive.endsAt(NOON_FIVE)); + + assertFalse(fiveToTen.contains(NOON)); + assertTrue(fiveToTen.contains(NOON_FIVE)); + assertTrue(fiveToTen.contains(NOON_FIVE.plus(1, MINUTES))); + assertFalse(fiveToTen.contains(NOON_TEN)); + assertFalse(fiveToTen.contains(NOON_TEN.plus(1, MINUTES))); + + assertFalse(fiveToTen.isBefore(NOON)); + assertFalse(fiveToTen.isBefore(NOON_FIVE)); + assertFalse(fiveToTen.isBefore(NOON_TEN)); + assertTrue(fiveToTen.isBefore(NOON_TEN.plus(1, MINUTES))); + + assertTrue(fiveToTen.endsAt(NOON_TEN)); + } + + @Test + void canCreateSlotFromSeedWithinThatSlot() { + //expect + assertEquals(TimeSlot.of(NOON, NOON_FIVE), slotThatContains(NOON.plus(1, MINUTES))); + assertEquals(TimeSlot.of(NOON, NOON_FIVE), slotThatContains(NOON.plus(2, MINUTES))); + assertEquals(TimeSlot.of(NOON, NOON_FIVE), slotThatContains(NOON.plus(3, MINUTES))); + assertEquals(TimeSlot.of(NOON, NOON_FIVE), slotThatContains(NOON.plus(4, MINUTES))); + + assertEquals(TimeSlot.of(NOON_FIVE, NOON_TEN), slotThatContains(NOON_FIVE.plus(1, MINUTES))); + assertEquals(TimeSlot.of(NOON_FIVE, NOON_TEN), slotThatContains(NOON_FIVE.plus(2, MINUTES))); + assertEquals(TimeSlot.of(NOON_FIVE, NOON_TEN), slotThatContains(NOON_FIVE.plus(3, MINUTES))); + } +} \ No newline at end of file diff --git a/src/test/java/io/legacyfighter/cabs/entity/CalculateTransitPriceTest.java b/src/test/java/io/legacyfighter/cabs/entity/CalculateTransitPriceTest.java index 9eb17a8..35319d2 100644 --- a/src/test/java/io/legacyfighter/cabs/entity/CalculateTransitPriceTest.java +++ b/src/test/java/io/legacyfighter/cabs/entity/CalculateTransitPriceTest.java @@ -7,7 +7,7 @@ import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.OffsetDateTime; import static io.legacyfighter.cabs.entity.Transit.Status.*; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -46,7 +46,7 @@ void calculatePriceOnRegularDay() { Money price = transit.calculateFinalCosts(); //then - assertEquals( new Money(2900), price); //29.00 + assertEquals(new Money(2900), price); //29.00 } @Test @@ -124,27 +124,27 @@ Transit transit(Status status, int km) { } void transitWasOnDoneOnFriday(Transit transit) { - transit.setDateTime(LocalDateTime.of(2021, 4, 16, 8, 30).toInstant(ZoneOffset.UTC)); + transit.setDateTime(LocalDateTime.of(2021, 4, 16, 8, 30).toInstant(OffsetDateTime.now().getOffset())); } void transitWasDoneOnNewYearsEve(Transit transit) { - transit.setDateTime(LocalDateTime.of(2021, 12, 31, 8, 30).toInstant(ZoneOffset.UTC)); + transit.setDateTime(LocalDateTime.of(2021, 12, 31, 8, 30).toInstant(OffsetDateTime.now().getOffset())); } void transitWasDoneOnSaturday(Transit transit) { - transit.setDateTime(LocalDateTime.of(2021, 4, 17, 8, 30).toInstant(ZoneOffset.UTC)); + transit.setDateTime(LocalDateTime.of(2021, 4, 17, 8, 30).toInstant(OffsetDateTime.now().getOffset())); } void transitWasDoneOnSunday(Transit transit) { - transit.setDateTime(LocalDateTime.of(2021, 4, 18, 8, 30).toInstant(ZoneOffset.UTC)); + transit.setDateTime(LocalDateTime.of(2021, 4, 18, 8, 30).toInstant(OffsetDateTime.now().getOffset())); } void transitWasDoneOnSaturdayNight(Transit transit) { - transit.setDateTime(LocalDateTime.of(2021, 4, 17, 19, 30).toInstant(ZoneOffset.UTC)); + transit.setDateTime(LocalDateTime.of(2021, 4, 17, 19, 30).toInstant(OffsetDateTime.now().getOffset())); } void transitWasDoneIn2018(Transit transit) { - transit.setDateTime(LocalDateTime.of(2018, 1,1, 8, 30).toInstant(ZoneOffset.UTC)); + transit.setDateTime(LocalDateTime.of(2018, 1, 1, 8, 30).toInstant(OffsetDateTime.now().getOffset())); } diff --git a/src/test/java/io/legacyfighter/cabs/entity/miles/MilesTest.java b/src/test/java/io/legacyfighter/cabs/entity/miles/MilesTest.java index 2241ba0..471ba00 100644 --- a/src/test/java/io/legacyfighter/cabs/entity/miles/MilesTest.java +++ b/src/test/java/io/legacyfighter/cabs/entity/miles/MilesTest.java @@ -4,17 +4,17 @@ import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import static io.legacyfighter.cabs.entity.miles.ConstantUntil.constantUntil; import static io.legacyfighter.cabs.entity.miles.ConstantUntil.constantUntilForever; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class MilesTest { - static Instant YESTERDAY = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(ZoneOffset.UTC); + static Instant YESTERDAY = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); static Instant TODAY = YESTERDAY.plus(1, ChronoUnit.DAYS); static Instant TOMORROW = TODAY.plus(1, ChronoUnit.DAYS); diff --git a/src/test/java/io/legacyfighter/cabs/integration/AwardMilesManagementIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/AwardMilesManagementIntegrationTest.java index 689cf05..f1e6ec8 100644 --- a/src/test/java/io/legacyfighter/cabs/integration/AwardMilesManagementIntegrationTest.java +++ b/src/test/java/io/legacyfighter/cabs/integration/AwardMilesManagementIntegrationTest.java @@ -2,9 +2,9 @@ import io.legacyfighter.cabs.common.Fixtures; import io.legacyfighter.cabs.dto.AwardsAccountDTO; -import io.legacyfighter.cabs.entity.miles.AwardedMiles; import io.legacyfighter.cabs.entity.Client; import io.legacyfighter.cabs.entity.Transit; +import io.legacyfighter.cabs.entity.miles.AwardedMiles; import io.legacyfighter.cabs.money.Money; import io.legacyfighter.cabs.repository.AwardsAccountRepository; import io.legacyfighter.cabs.service.AwardsService; @@ -17,11 +17,10 @@ import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.OffsetDateTime; import java.util.List; import static io.legacyfighter.cabs.entity.Client.Type.NORMAL; -import static io.legacyfighter.cabs.entity.Client.Type.VIP; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.when; @@ -29,7 +28,7 @@ @SpringBootTest class AwardMilesManagementIntegrationTest { - static Instant NOW = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(ZoneOffset.UTC); + static Instant NOW = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); @Autowired AwardsService awardsService; @@ -165,7 +164,7 @@ void canTransferMiles() { fixtures.activeAwardsAccount(client); fixtures.activeAwardsAccount(secondClient); //and - awardsService.registerNonExpiringMiles(client.getId(), 10); + awardsService.registerNonExpiringMiles(client.getId(), 10); //when awardsService.transferMiles(client.getId(), secondClient.getId(), 10); @@ -186,7 +185,7 @@ void cannotTransferMilesWhenAccountIsNotActive() { fixtures.activeAwardsAccount(client); fixtures.activeAwardsAccount(secondClient); //and - awardsService.registerNonExpiringMiles(client.getId(), 10); + awardsService.registerNonExpiringMiles(client.getId(), 10); //and awardsService.deactivateAccount(client.getId()); @@ -206,7 +205,7 @@ void cannotTransferMilesWhenNotEnough() { fixtures.activeAwardsAccount(client); fixtures.activeAwardsAccount(secondClient); //and - awardsService.registerNonExpiringMiles(client.getId(), 10); + awardsService.registerNonExpiringMiles(client.getId(), 10); //when awardsService.transferMiles(client.getId(), secondClient.getId(), 30); @@ -224,7 +223,7 @@ void cannotTransferMilesWhenAccountNotActive() { fixtures.activeAwardsAccount(client); fixtures.activeAwardsAccount(secondClient); //and - awardsService.registerNonExpiringMiles(client.getId(), 10); + awardsService.registerNonExpiringMiles(client.getId(), 10); //and awardsService.deactivateAccount(client.getId()); diff --git a/src/test/java/io/legacyfighter/cabs/integration/CalculateDriverTravelledDistanceIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/CalculateDriverTravelledDistanceIntegrationTest.java new file mode 100644 index 0000000..85eeffc --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/integration/CalculateDriverTravelledDistanceIntegrationTest.java @@ -0,0 +1,149 @@ +package io.legacyfighter.cabs.integration; + +import io.legacyfighter.cabs.common.Fixtures; +import io.legacyfighter.cabs.distance.Distance; +import io.legacyfighter.cabs.driverreport.travelleddistance.TravelledDistanceService; +import io.legacyfighter.cabs.entity.Driver; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@SpringBootTest +class CalculateDriverTravelledDistanceIntegrationTest { + + static Instant NOON = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); + static Instant NOON_FIVE = NOON.plus(5, ChronoUnit.MINUTES); + static final Instant NOON_TEN = NOON_FIVE.plus(5, ChronoUnit.MINUTES); + + @Autowired + TravelledDistanceService travelledDistanceService; + + @Autowired + Fixtures fixtures; + + @MockBean + Clock clock; + + @Test + void distanceIsZeroWhenZeroPositions() { + //given + Driver driver = fixtures.aDriver(); + + //when + Distance distance = travelledDistanceService.calculateDistance(driver.getId(), NOON, NOON_FIVE); + + //then + assertEquals("0km", distance.printIn("km")); + } + + @Test + void travelledDistanceWithoutMultiplePositionsIzZero() { + //given + Driver driver = fixtures.aDriver(); + //and + itIsNoon(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + + //when + Distance distance = travelledDistanceService.calculateDistance(driver.getId(), NOON, NOON_FIVE); + + //then + assertEquals("0km", distance.printIn("km")); + } + + @Test + void canCalculateTravelledDistanceFromShortTransit() { + //given + Driver driver = fixtures.aDriver(); + //and + itIsNoon(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + travelledDistanceService.addPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON); + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + + //when + Distance distance = travelledDistanceService.calculateDistance(driver.getId(), NOON, NOON_FIVE); + + //then + assertEquals("4.009km", distance.printIn("km")); + } + + @Test + void canCalculateTravelledDistanceWithBreakWithin() { + //given + Driver driver = fixtures.aDriver(); + //and + itIsNoon(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + travelledDistanceService.addPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON); + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + //and + itIsNoonFive(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON_FIVE); + travelledDistanceService.addPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON_FIVE); + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON_FIVE); + + //when + Distance distance = travelledDistanceService.calculateDistance(driver.getId(), NOON, NOON_FIVE); + + //then + assertEquals("8.017km", distance.printIn("km")); + } + + @Test + void canCalculateTravelledDistanceWithMultipleBreaks() { + //given + Driver driver = fixtures.aDriver(); + //and + itIsNoon(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + travelledDistanceService.addPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON); + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + //and + itIsNoonFive(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON_FIVE); + travelledDistanceService.addPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON_FIVE); + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON_FIVE); + //and + itIsNoonTen(); + //and + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON_TEN); + travelledDistanceService.addPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON_TEN); + travelledDistanceService.addPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON_TEN); + + //when + Distance distance = travelledDistanceService.calculateDistance(driver.getId(), NOON, NOON_TEN); + + //then + assertEquals("12.026km", distance.printIn("km")); + } + + void itIsNoon() { + when(clock.instant()).thenReturn(NOON); + } + + void itIsNoonFive() { + when(clock.instant()).thenReturn(NOON_FIVE); + } + + void itIsNoonTen() { + when(clock.instant()).thenReturn(NOON_TEN); + } + +} diff --git a/src/test/java/io/legacyfighter/cabs/integration/CreateDriverReportIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/CreateDriverReportIntegrationTest.java new file mode 100644 index 0000000..2e82155 --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/integration/CreateDriverReportIntegrationTest.java @@ -0,0 +1,191 @@ +package io.legacyfighter.cabs.integration; + +import io.legacyfighter.cabs.common.Fixtures; +import io.legacyfighter.cabs.driverreport.DriverReportController; +import io.legacyfighter.cabs.dto.CarTypeDTO; +import io.legacyfighter.cabs.dto.DriverAttributeDTO; +import io.legacyfighter.cabs.dto.DriverReport; +import io.legacyfighter.cabs.dto.TransitDTO; +import io.legacyfighter.cabs.entity.*; +import io.legacyfighter.cabs.entity.CarType.CarClass; +import io.legacyfighter.cabs.repository.AddressRepository; +import io.legacyfighter.cabs.service.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import static io.legacyfighter.cabs.entity.CarType.CarClass.PREMIUM; +import static io.legacyfighter.cabs.entity.CarType.CarClass.VAN; +import static io.legacyfighter.cabs.entity.Driver.Status.ACTIVE; +import static io.legacyfighter.cabs.entity.DriverAttribute.DriverAttributeName.*; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@SpringBootTest +class CreateDriverReportIntegrationTest { + + static Instant DAY_BEFORE_YESTERDAY = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); + static Instant YESTERDAY = DAY_BEFORE_YESTERDAY.plus(1, ChronoUnit.DAYS); + static Instant TODAY = YESTERDAY.plus(1, ChronoUnit.DAYS); + + @Autowired + TransitService transitService; + + @Autowired + DriverTrackingService driverTrackingService; + + @Autowired + DriverSessionService driverSessionService; + + @Autowired + CarTypeService carTypeService; + + @Autowired + Fixtures fixtures; + + @Autowired + DriverReportController driverReportController; + + @Autowired + AddressRepository addressRepository; + + @MockBean + GeocodingService geocodingService; + + @Autowired + ClaimService claimService; + + @MockBean + Clock clock; + + @BeforeEach + public void setup() { + anActiveCarCategory(VAN); + anActiveCarCategory(PREMIUM); + } + + @Test + void shouldCreateDriversReport() { + //given + Client client = fixtures.aClient(); + //and + Driver driver = aDriver(ACTIVE, "JAN", "NOWAK", "FARME100165AB5EW"); + //and + fixtures.driverHasAttribute(driver, COMPANY_NAME, "UBER"); + fixtures.driverHasAttribute(driver, PENALTY_POINTS, "21"); + fixtures.driverHasAttribute(driver, MEDICAL_EXAMINATION_REMARKS, "private info"); + //and + driverHasDoneSessionAndPicksSomeoneUpInCar(driver, client, VAN, "WU1213", "SCODA FABIA", TODAY); + driverHasDoneSessionAndPicksSomeoneUpInCar(driver, client, VAN, "WU1213", "SCODA OCTAVIA", YESTERDAY); + Transit inBmw = driverHasDoneSessionAndPicksSomeoneUpInCar(driver, client, VAN, "WU1213", "BMW M2", DAY_BEFORE_YESTERDAY); + //and + fixtures.createClaim(client, inBmw, "za szybko"); + + //when + DriverReport driverReportWithin2days = loadReportIncludingPastDays(driver, 2); + DriverReport driverReportWithin1day = loadReportIncludingPastDays(driver, 1); + DriverReport driverReportForJustToday = loadReportIncludingPastDays(driver, 0); + + //then + assertEquals(3, driverReportWithin2days.getSessions().keySet().size()); + assertEquals(2, driverReportWithin1day.getSessions().keySet().size()); + assertEquals(1, driverReportForJustToday.getSessions().keySet().size()); + + + assertEquals("FARME100165AB5EW", driverReportWithin2days.getDriverDTO().getDriverLicense()); + assertEquals("JAN", driverReportWithin2days.getDriverDTO().getFirstName()); + assertEquals("NOWAK", driverReportWithin2days.getDriverDTO().getLastName()); + assertEquals(2, driverReportWithin2days.getAttributes().size()); + assertTrue(driverReportWithin2days.getAttributes().contains(new DriverAttributeDTO(COMPANY_NAME, "UBER"))); + assertTrue(driverReportWithin2days.getAttributes().contains(new DriverAttributeDTO(PENALTY_POINTS, "21"))); + + assertThat(transitsInSessionIn("SCODA FABIA", driverReportWithin2days)) + .hasSize(1); + assertThat(transitsInSessionIn("SCODA FABIA", driverReportWithin2days).get(0).getClaimDTO()).isNull(); + + assertThat(transitsInSessionIn("SCODA OCTAVIA", driverReportWithin2days)) + .hasSize(1); + assertThat(transitsInSessionIn("SCODA OCTAVIA", driverReportWithin2days).get(0).getClaimDTO()).isNull(); + + assertThat(transitsInSessionIn("BMW M2", driverReportWithin2days)) + .hasSize(1); + assertThat(transitsInSessionIn("BMW M2", driverReportWithin2days).get(0).getClaimDTO()).isNotNull(); + assertThat(transitsInSessionIn("BMW M2", driverReportWithin2days).get(0).getClaimDTO().getReason()).isEqualTo("za szybko"); + } + + DriverReport loadReportIncludingPastDays(Driver driver, int days) { + Mockito.when(clock.instant()).thenReturn(TODAY); + DriverReport driverReport = driverReportController.loadReportForDriver(driver.getId(), days); + return driverReport; + } + + List transitsInSessionIn(String carBrand, DriverReport driverReport) { + return driverReport + .getSessions() + .entrySet() + .stream() + .filter(e -> e.getKey().getCarBrand().equals(carBrand)) + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .collect(toList()); + } + + Transit driverHasDoneSessionAndPicksSomeoneUpInCar(Driver driver, Client client, CarClass carClass, String plateNumber, String carBrand, Instant when) { + when(clock.instant()).thenReturn(when); + Long driverId = driver.getId(); + driverSessionService.logIn(driverId, plateNumber, carClass, carBrand); + driverTrackingService.registerPosition(driverId, 10, 20, Instant.now()); + Transit transit = transitService.createTransit(client.getId(), address("PL", "MAZ", "WAW", "STREET", 1, 10, 20), address("PL", "MAZ", "WAW", "STREET", 100, 10.01, 20.01), carClass); + transitService.publishTransit(transit.getId()); + transitService.acceptTransit(driverId, transit.getId()); + transitService.startTransit(driverId, transit.getId()); + transitService.completeTransit(driverId, transit.getId(), address("PL", "MAZ", "WAW", "STREET", 100, 10.01, 20.01)); + driverSessionService.logOutCurrentSession(driverId); + return transit; + } + + + Driver aDriver(Driver.Status status, String name, String lastName, String driverLicense) { + Driver driver = fixtures.aDriver(ACTIVE, name, lastName, driverLicense); + fixtures.driverHasFee(driver, DriverFee.FeeType.FLAT, 10); + return driver; + } + + Address address(String country, String district, String city, String street, Integer buildingNumber, double latitude, double longitude) { + Address address = new Address(); + address.setCountry(country); + address.setDistrict(district); + address.setCity(city); + address.setStreet(street); + address.setBuildingNumber(buildingNumber); + when(geocodingService.geocodeAddress(address)).thenReturn(new double[]{latitude, longitude}); + return addressRepository.save(address); + } + + CarType anActiveCarCategory(CarClass carClass) { + CarTypeDTO carTypeDTO = new CarTypeDTO(); + carTypeDTO.setCarClass(carClass); + carTypeDTO.setDescription("opis"); + CarType carType = carTypeService.create(carTypeDTO); + IntStream.range(1, carType.getMinNoOfCarsToActivateClass() + 1) + .forEach(i -> carTypeService.registerCar(carType.getCarClass())); + carTypeService.activate(carType.getId()); + return carType; + } +} \ No newline at end of file diff --git a/src/test/java/io/legacyfighter/cabs/integration/DriverTrackingServiceIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/DriverTrackingServiceIntegrationTest.java new file mode 100644 index 0000000..0feca3b --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/integration/DriverTrackingServiceIntegrationTest.java @@ -0,0 +1,58 @@ +package io.legacyfighter.cabs.integration; + +import io.legacyfighter.cabs.common.Fixtures; +import io.legacyfighter.cabs.distance.Distance; +import io.legacyfighter.cabs.entity.Driver; +import io.legacyfighter.cabs.service.DriverTrackingService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@SpringBootTest +class DriverTrackingServiceIntegrationTest { + + static Instant NOON = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); + static Instant NOON_FIVE = NOON.plus(5, ChronoUnit.MINUTES); + + @Autowired + DriverTrackingService driverTrackingService; + + @Autowired + Fixtures fixtures; + + @MockBean + Clock clock; + + @Test + void canCalculateTravelledDistanceFromShortTransit() { + //given + Driver driver = fixtures.aDriver(); + //and + itIsNoon(); + //and + driverTrackingService.registerPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + driverTrackingService.registerPosition(driver.getId(), 53.31861111111111, -1.6997222222222223, NOON); + driverTrackingService.registerPosition(driver.getId(), 53.32055555555556, -1.7297222222222221, NOON); + + //when + Distance distance = driverTrackingService.calculateTravelledDistance(driver.getId(), NOON, NOON_FIVE); + + //then + assertEquals("4.009km", distance.printIn("km")); + } + + void itIsNoon() { + when(clock.instant()).thenReturn(NOON); + } + +} diff --git a/src/test/java/io/legacyfighter/cabs/integration/ExpiringMilesIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/ExpiringMilesIntegrationTest.java index 613e5ae..5c96f04 100644 --- a/src/test/java/io/legacyfighter/cabs/integration/ExpiringMilesIntegrationTest.java +++ b/src/test/java/io/legacyfighter/cabs/integration/ExpiringMilesIntegrationTest.java @@ -14,7 +14,7 @@ import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -23,7 +23,7 @@ @SpringBootTest class ExpiringMilesIntegrationTest { - static Instant _1989_12_12 = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(ZoneOffset.UTC); + static Instant _1989_12_12 = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); static Instant _1989_12_13 = _1989_12_12.plus(1, ChronoUnit.DAYS); static Instant _1989_12_14 = _1989_12_13.plus(1, ChronoUnit.DAYS); diff --git a/src/test/java/io/legacyfighter/cabs/integration/RemovingAwardMilesIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/RemovingAwardMilesIntegrationTest.java index 14aeadb..7ddf553 100644 --- a/src/test/java/io/legacyfighter/cabs/integration/RemovingAwardMilesIntegrationTest.java +++ b/src/test/java/io/legacyfighter/cabs/integration/RemovingAwardMilesIntegrationTest.java @@ -2,9 +2,9 @@ import io.legacyfighter.cabs.common.Fixtures; import io.legacyfighter.cabs.config.AppProperties; -import io.legacyfighter.cabs.entity.miles.AwardedMiles; import io.legacyfighter.cabs.entity.Client; import io.legacyfighter.cabs.entity.Transit; +import io.legacyfighter.cabs.entity.miles.AwardedMiles; import io.legacyfighter.cabs.money.Money; import io.legacyfighter.cabs.repository.AwardsAccountRepository; import io.legacyfighter.cabs.service.AwardsService; @@ -16,7 +16,7 @@ import java.time.Clock; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.stream.Stream; @@ -29,10 +29,10 @@ @SpringBootTest class RemovingAwardMilesIntegrationTest { - static Instant DAY_BEFORE_YESTERDAY = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(ZoneOffset.UTC); + static Instant DAY_BEFORE_YESTERDAY = LocalDateTime.of(1989, 12, 12, 12, 12).toInstant(OffsetDateTime.now().getOffset()); static Instant YESTERDAY = DAY_BEFORE_YESTERDAY.plus(1, ChronoUnit.DAYS); static Instant TODAY = YESTERDAY.plus(1, ChronoUnit.DAYS); - static Instant SUNDAY = LocalDateTime.of(1989, 12, 17, 12, 12).toInstant(ZoneOffset.UTC); + static Instant SUNDAY = LocalDateTime.of(1989, 12, 17, 12, 12).toInstant(OffsetDateTime.now().getOffset()); @Autowired AwardsService awardsService; diff --git a/src/test/java/io/legacyfighter/cabs/integration/TariffRecognizingIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/TariffRecognizingIntegrationTest.java index fe59f13..c0918eb 100644 --- a/src/test/java/io/legacyfighter/cabs/integration/TariffRecognizingIntegrationTest.java +++ b/src/test/java/io/legacyfighter/cabs/integration/TariffRecognizingIntegrationTest.java @@ -9,7 +9,8 @@ import org.springframework.boot.test.context.SpringBootTest; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.time.Month; +import java.time.OffsetDateTime; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -25,7 +26,7 @@ class TariffRecognizingIntegrationTest { @Test void newYearsEveTariffShouldBeDisplayed() { //given - Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 12, 31, 8, 30).toInstant(ZoneOffset.UTC)); + Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 12, 31, 8, 30).toInstant(OffsetDateTime.now().getOffset())); //when TransitDTO transitDTO = transitController.getTransit(transit.getId()); @@ -39,7 +40,7 @@ void newYearsEveTariffShouldBeDisplayed() { @Test void weekendTariffShouldBeDisplayed() { //given - Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 4, 17, 8, 30).toInstant(ZoneOffset.UTC)); + Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 4, 17, 8, 30).toInstant(OffsetDateTime.now().getOffset())); //when TransitDTO transitDTO = transitController.getTransit(transit.getId()); @@ -52,7 +53,7 @@ void weekendTariffShouldBeDisplayed() { @Test void weekendPlusTariffShouldBeDisplayed() { //given - Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 4, 17, 22, 30).toInstant(ZoneOffset.UTC)); + Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2022, Month.FEBRUARY, 20, 6, 0).toInstant(OffsetDateTime.now().getOffset())); //when TransitDTO transitDTO = transitController.getTransit(transit.getId()); @@ -65,7 +66,7 @@ void weekendPlusTariffShouldBeDisplayed() { @Test void standardTariffShouldBeDisplayed() { //given - Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 4, 13, 22, 30).toInstant(ZoneOffset.UTC)); + Transit transit = fixtures.aCompletedTransitAt(60, LocalDateTime.of(2021, 4, 13, 22, 30).toInstant(OffsetDateTime.now().getOffset())); //when TransitDTO transitDTO = transitController.getTransit(transit.getId()); diff --git a/src/test/java/io/legacyfighter/cabs/integration/TransitLifeCycleIntegrationTest.java b/src/test/java/io/legacyfighter/cabs/integration/TransitLifeCycleIntegrationTest.java index f499618..fe78617 100644 --- a/src/test/java/io/legacyfighter/cabs/integration/TransitLifeCycleIntegrationTest.java +++ b/src/test/java/io/legacyfighter/cabs/integration/TransitLifeCycleIntegrationTest.java @@ -14,6 +14,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import java.time.Instant; + import static io.legacyfighter.cabs.entity.CarType.CarClass.VAN; import static io.legacyfighter.cabs.entity.Transit.Status.*; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -448,7 +450,7 @@ Long aNearbyDriver(String plateNumber) { Driver driver = fixtures.aDriver(); fixtures.driverHasFee(driver, DriverFee.FeeType.FLAT, 10); driverSessionService.logIn(driver.getId(), plateNumber, VAN, "BRAND"); - driverTrackingService.registerPosition(driver.getId(), 1, 1); + driverTrackingService.registerPosition(driver.getId(), 1, 1, Instant.now()); return driver.getId(); } @@ -456,7 +458,7 @@ Long aFarAwayDriver(String plateNumber) { Driver driver = fixtures.aDriver(); fixtures.driverHasFee(driver, DriverFee.FeeType.FLAT, 10); driverSessionService.logIn(driver.getId(), plateNumber, VAN, "BRAND"); - driverTrackingService.registerPosition(driver.getId(), 1000, 1000); + driverTrackingService.registerPosition(driver.getId(), 1000, 1000, Instant.now()); return driver.getId(); } diff --git a/src/test/java/io/legacyfighter/cabs/repair/api/RepairProcessTest.java b/src/test/java/io/legacyfighter/cabs/repair/api/RepairProcessTest.java new file mode 100644 index 0000000..a3ce971 --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/repair/api/RepairProcessTest.java @@ -0,0 +1,48 @@ +package io.legacyfighter.cabs.repair.api; + +import io.legacyfighter.cabs.party.api.PartyId; +import io.legacyfighter.cabs.repair.legacy.parts.Parts; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Set; + +@SpringBootTest +public class RepairProcessTest { + + @Autowired + private RepairProcess vehicleRepairProcess; + + @Autowired + private ContractManager contractManager; + + private final PartyId vehicle = new PartyId(); + private final PartyId handlingParty = new PartyId(); + + @Test + public void warrantyByInsuranceCoversAllButPaint(){ + //given + contractManager.extendedWarrantyContractSigned(handlingParty, vehicle); + + Set parts = Set.of(new Parts[] {Parts.ENGINE, Parts.GEARBOX, Parts.PAINT, Parts.SUSPENSION}); + RepairRequest repairRequest = new RepairRequest(vehicle, parts); + //when + ResolveResult result = vehicleRepairProcess.resolve(repairRequest); + //then + new VehicleRepairAssert(result).by(handlingParty).free().allPartsBut(parts, new Parts[] {Parts.PAINT}); + } + + @Test + public void manufacturerWarrantyCoversAll(){ + //given + contractManager.manufacturerWarrantyRegistered(handlingParty, vehicle); + + Set parts = Set.of(new Parts[]{Parts.ENGINE, Parts.GEARBOX, Parts.PAINT, Parts.SUSPENSION}); + RepairRequest repairRequest = new RepairRequest(vehicle, parts); + //when + ResolveResult result = vehicleRepairProcess.resolve(repairRequest); + //then + new VehicleRepairAssert(result).by(handlingParty).free().allParts(parts); + } +} diff --git a/src/test/java/io/legacyfighter/cabs/repair/api/VehicleRepairAssert.java b/src/test/java/io/legacyfighter/cabs/repair/api/VehicleRepairAssert.java new file mode 100644 index 0000000..31e8f44 --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/repair/api/VehicleRepairAssert.java @@ -0,0 +1,51 @@ +package io.legacyfighter.cabs.repair.api; + +import io.legacyfighter.cabs.money.Money; +import io.legacyfighter.cabs.party.api.PartyId; +import io.legacyfighter.cabs.repair.legacy.parts.Parts; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class VehicleRepairAssert { + private ResolveResult result; + + public VehicleRepairAssert(ResolveResult result) { + this(result, true); + } + + public VehicleRepairAssert(ResolveResult result, boolean demandSuccess) { + this.result = result; + if (demandSuccess) + assertEquals(ResolveResult.Status.SUCCESS, result.getStatus()); + else + assertEquals(ResolveResult.Status.ERROR, result.getStatus()); + } + + public VehicleRepairAssert free() { + assertEquals(Money.ZERO, result.getTotalCost()); + return this; + } + + public VehicleRepairAssert allParts(Set parts) { + assertEquals(parts, result.getAcceptedParts()); + return this; + } + + public VehicleRepairAssert by(PartyId handlingParty) { + assertEquals(handlingParty.toUUID(), result.getHandlingParty()); + return this; + } + + public VehicleRepairAssert allPartsBut(Set parts, Parts[] excludedParts) { + Set exptectedParts = new HashSet<>(parts); + exptectedParts.removeAll(Arrays.stream(excludedParts).collect(Collectors.toSet())); + + assertEquals(exptectedParts, result.getAcceptedParts()); + return this; + } +} diff --git a/src/test/java/io/legacyfighter/cabs/repair/legacy/job/RepairTest.java b/src/test/java/io/legacyfighter/cabs/repair/legacy/job/RepairTest.java new file mode 100644 index 0000000..97c1621 --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/repair/legacy/job/RepairTest.java @@ -0,0 +1,46 @@ +package io.legacyfighter.cabs.repair.legacy.job; + +import io.legacyfighter.cabs.money.Money; +import io.legacyfighter.cabs.repair.legacy.parts.Parts; +import io.legacyfighter.cabs.repair.legacy.user.EmployeeDriverWithOwnCar; +import io.legacyfighter.cabs.repair.legacy.user.SignedContract; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class RepairTest { + + @Test + public void employeeDriverWithOwnCarCoveredByWarrantyShouldRepairForFree(){ + //given + EmployeeDriverWithOwnCar employee = new EmployeeDriverWithOwnCar(); + employee.setContract(fullCoverageWarranty()); + //when + JobResult result = employee.doJob(fullRepair()); + //then + assertEquals(JobResult.Decision.ACCEPTED, result.getDecision()); + assertEquals(Money.ZERO, result.getParam("totalCost")); + assertEquals(allParts(), result.getParam("acceptedParts")); + } + + private RepairJob fullRepair() { + RepairJob job = new RepairJob(); + job.setEstimatedValue(new Money(50000)); + job.setPartsToRepair(allParts()); + return job; + } + + private SignedContract fullCoverageWarranty() { + SignedContract contract = new SignedContract(); + contract.setCoverageRatio(100.0); + contract.setCoveredParts(allParts()); + return contract; + } + + private Set allParts(){ + return Set.of(Parts.values()); + } +} diff --git a/src/test/java/io/legacyfighter/cabs/repair/legacy/service/JobDoerTest.java b/src/test/java/io/legacyfighter/cabs/repair/legacy/service/JobDoerTest.java new file mode 100644 index 0000000..522da63 --- /dev/null +++ b/src/test/java/io/legacyfighter/cabs/repair/legacy/service/JobDoerTest.java @@ -0,0 +1,45 @@ +package io.legacyfighter.cabs.repair.legacy.service; + +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 org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +public class JobDoerTest { + + /** + * fake database returns {@link io.legacyfighter.cabs.repair.legacy.user.EmployeeDriverWithOwnCar} + */ + private static final Long ANY_USER = 1L; + + @Autowired + JobDoer jobDoer; + + @Test + public void employeeWithOwnCarWithWarrantyShouldHaveCoveredAllPartsForFree(){ + JobResult result = jobDoer.repair(ANY_USER, repairJob()); + + assertEquals(result.getDecision(), JobResult.Decision.ACCEPTED); + assertEquals(result.getParam("acceptedParts"), allParts()); + assertEquals(result.getParam("totalCost"), Money.ZERO); + } + + private RepairJob repairJob() { + RepairJob job= new RepairJob(); + job.setPartsToRepair(allParts()); + job.setEstimatedValue(new Money(7000)); + return job; + } + + Set allParts(){ + return Set.of(Parts.values()); + } +}