├── .gitignore ├── .travis.yml ├── LICENSE ├── ddd-core ├── pom.xml └── src │ ├── main │ └── java │ │ └── xyz │ │ └── zhangyi │ │ └── ddd │ │ └── core │ │ ├── domain │ │ ├── AbstractIdentity.java │ │ └── Identity.java │ │ ├── event │ │ ├── ApplicationEvent.java │ │ └── Event.java │ │ ├── exception │ │ ├── ApplicationDomainException.java │ │ ├── ApplicationException.java │ │ ├── ApplicationInfrastructureException.java │ │ ├── ApplicationValidationException.java │ │ └── DomainException.java │ │ ├── gateway │ │ ├── north │ │ │ └── Resources.java │ │ └── south │ │ │ ├── adapter │ │ │ └── EventKafkaPublisher.java │ │ │ └── port │ │ │ ├── Destination.java │ │ │ └── EventPublisher.java │ │ └── stereotype │ │ ├── Adapter.java │ │ ├── Aggregate.java │ │ ├── BoundedContext.java │ │ ├── Direction.java │ │ ├── DomainService.java │ │ ├── Local.java │ │ ├── MessageContract.java │ │ ├── Port.java │ │ ├── PortType.java │ │ ├── Remote.java │ │ ├── RemoteType.java │ │ └── SubDomain.java │ └── test │ └── java │ └── xyz │ └── zhangyi │ └── ddd │ └── core │ └── gateway │ └── south │ └── adapter │ └── EventKafkaPublisherTest.java ├── eas-employee ├── pom.xml └── src │ ├── main │ └── java │ │ └── xyz │ │ └── zhangyi │ │ └── ddd │ │ └── eas │ │ └── valueadded │ │ └── employeecontext │ │ ├── domain │ │ ├── attendance │ │ │ ├── Attendance.java │ │ │ ├── AttendanceStatus.java │ │ │ ├── Leave.java │ │ │ ├── LeaveType.java │ │ │ ├── TimeCard.java │ │ │ └── WorkTimeRule.java │ │ ├── employee │ │ │ ├── Employee.java │ │ │ ├── EmployeeId.java │ │ │ ├── EmployeeIdGenerator.java │ │ │ ├── Gender.java │ │ │ ├── IDCard.java │ │ │ └── Phone.java │ │ └── exception │ │ │ ├── InvalidAttendanceException.java │ │ │ ├── InvalidEmployeeException.java │ │ │ ├── InvalidEmployeeIdException.java │ │ │ ├── InvalidIdCardException.java │ │ │ └── InvalidPhoneNumberException.java │ │ ├── south │ │ └── port │ │ │ └── repository │ │ │ └── EmployeeRepository.java │ │ └── utils │ │ └── DateTimes.java │ └── test │ └── java │ └── xyz │ └── zhangyi │ └── ddd │ └── eas │ └── valueadded │ └── employeecontext │ └── domain │ ├── attendance │ └── AttendanceTest.java │ └── employee │ ├── EmployeeIdGeneratorTest.java │ ├── EmployeeIdTest.java │ ├── EmployeeTest.java │ ├── IDCardTest.java │ └── PhoneTest.java ├── eas-entry ├── pom.xml └── src │ └── main │ ├── java │ └── xyz │ │ └── zhangyi │ │ └── ddd │ │ └── eas │ │ └── EasApplication.java │ └── resources │ └── application.yml ├── eas-project ├── pom.xml └── src │ ├── main │ └── java │ │ └── xyz │ │ └── zhangyi │ │ └── ddd │ │ └── eas │ │ └── projectcontext │ │ ├── domain │ │ ├── changehistory │ │ │ ├── ChangeHistory.java │ │ │ ├── ChangeHistoryId.java │ │ │ ├── Operation.java │ │ │ └── Operator.java │ │ ├── exception │ │ │ ├── AssignmentIssueException.java │ │ │ └── IssueException.java │ │ └── issue │ │ │ ├── Issue.java │ │ │ ├── IssueId.java │ │ │ ├── IssueOwner.java │ │ │ ├── IssueService.java │ │ │ └── IssueStatus.java │ │ └── south │ │ └── port │ │ ├── ChangeHistoryRepository.java │ │ └── IssueRepository.java │ └── test │ └── java │ └── xyz │ └── zhangyi │ └── ddd │ └── eas │ └── projectcontext │ └── domain │ └── issue │ ├── IssueServiceTest.java │ └── IssueTest.java ├── eas-training ├── pom.xml └── src │ ├── main │ ├── java │ │ └── xyz │ │ │ └── zhangyi │ │ │ └── ddd │ │ │ └── eas │ │ │ └── valueadded │ │ │ └── trainingcontext │ │ │ ├── domain │ │ │ ├── candidate │ │ │ │ └── Candidate.java │ │ │ ├── course │ │ │ │ └── CourseId.java │ │ │ ├── exception │ │ │ │ ├── NominationException.java │ │ │ │ └── TicketException.java │ │ │ ├── learning │ │ │ │ ├── Learning.java │ │ │ │ └── LearningService.java │ │ │ ├── notification │ │ │ │ ├── MailTemplate.java │ │ │ │ ├── MailTemplateException.java │ │ │ │ ├── NominationNotificationComposer.java │ │ │ │ ├── Notification.java │ │ │ │ ├── NotificationComposer.java │ │ │ │ ├── NotificationService.java │ │ │ │ ├── TemplateType.java │ │ │ │ ├── TemplateVariable.java │ │ │ │ └── VariableContext.java │ │ │ ├── ticket │ │ │ │ ├── NominationService.java │ │ │ │ ├── Nominator.java │ │ │ │ ├── Nominee.java │ │ │ │ ├── Ticket.java │ │ │ │ ├── TicketId.java │ │ │ │ ├── TicketService.java │ │ │ │ ├── TicketStatus.java │ │ │ │ └── TrainingRole.java │ │ │ ├── tickethistory │ │ │ │ ├── OperationType.java │ │ │ │ ├── Operator.java │ │ │ │ ├── StateTransit.java │ │ │ │ ├── TicketHistory.java │ │ │ │ ├── TicketOwner.java │ │ │ │ └── TicketOwnerType.java │ │ │ ├── training │ │ │ │ ├── Training.java │ │ │ │ ├── TrainingException.java │ │ │ │ ├── TrainingId.java │ │ │ │ └── TrainingService.java │ │ │ └── validate │ │ │ │ ├── ValidDate.java │ │ │ │ ├── ValidDateException.java │ │ │ │ └── ValidDateType.java │ │ │ ├── north │ │ │ ├── local │ │ │ │ └── appservice │ │ │ │ │ ├── NominationAppService.java │ │ │ │ │ └── TrainingAppService.java │ │ │ ├── message │ │ │ │ ├── NominatingCandidateRequest.java │ │ │ │ └── TrainingResponse.java │ │ │ └── remote │ │ │ │ └── resource │ │ │ │ ├── TicketResource.java │ │ │ │ └── TrainingResource.java │ │ │ └── south │ │ │ ├── adapter │ │ │ ├── client │ │ │ │ └── NotificationClientAdapter.java │ │ │ └── repository │ │ │ │ └── typehandler │ │ │ │ ├── CourseIdTypeHandler.java │ │ │ │ ├── TicketIdTypeHandler.java │ │ │ │ └── TrainingIdTypeHandler.java │ │ │ └── port │ │ │ ├── client │ │ │ └── NotificationClient.java │ │ │ └── repository │ │ │ ├── CandidateRepository.java │ │ │ ├── LearningRepository.java │ │ │ ├── MailTemplateRepository.java │ │ │ ├── TicketHistoryRepository.java │ │ │ ├── TicketRepository.java │ │ │ ├── TrainingRepository.java │ │ │ └── ValidDateRepository.java │ └── resources │ │ ├── db │ │ └── migration │ │ │ ├── V1__create_training_and_ticket_table.sql │ │ │ └── V2__insert_test_data.sql │ │ ├── mapper │ │ ├── CandidateMapper.xml │ │ ├── LearningMapper.xml │ │ ├── MailTemplateMapper.xml │ │ ├── TicketHistoryMapper.xml │ │ ├── TicketMapper.xml │ │ ├── TrainingMapper.xml │ │ └── ValidDateMapper.xml │ │ └── mybatis │ │ └── mybatis-config.xml │ └── test │ ├── java │ └── xyz │ │ └── zhangyi │ │ └── ddd │ │ └── eas │ │ └── valueadded │ │ └── trainingcontext │ │ ├── domain │ │ ├── learning │ │ │ └── LearningServiceTest.java │ │ ├── notification │ │ │ ├── MailTemplateTest.java │ │ │ └── NotificationServiceTest.java │ │ ├── ticket │ │ │ ├── NominationServiceTest.java │ │ │ ├── TicketServiceTest.java │ │ │ └── TicketTest.java │ │ ├── tickethistory │ │ │ └── TicketHistoryRepositoryIT.java │ │ └── training │ │ │ └── TrainingServiceTest.java │ │ └── north │ │ ├── local │ │ └── appservice │ │ │ └── NominationAppServiceIT.java │ │ └── message │ │ └── NominatingCandidateRequestTest.java │ └── resources │ └── spring-mybatis.xml ├── pom.xml └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .DS_Store 3 | *.log 4 | 5 | ### IntelliJ IDEA ### 6 | .idea 7 | *.iws 8 | *.iml 9 | *.ipr 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | 4 | jdk: 5 | - openjdk8 6 | 7 | script: "mvn cobertura:cobertura" 8 | 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /ddd-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | xyz.zhangyi.ddd 9 | eas 10 | 1.0-SNAPSHOT 11 | 12 | 13 | ddd-core 14 | 15 | 16 | 17 | org.apache.kafka 18 | kafka-clients 19 | 2.4.0 20 | 21 | 22 | com.alibaba 23 | fastjson 24 | 1.2.62 25 | 26 | 27 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/domain/AbstractIdentity.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.domain; 2 | 3 | import java.util.Objects; 4 | 5 | public abstract class AbstractIdentity implements Identity { 6 | private T value; 7 | 8 | protected AbstractIdentity(T value) { 9 | this.setId(value); 10 | } 11 | 12 | @Override 13 | public T value() { 14 | return value; 15 | } 16 | 17 | private void setId(T value) { 18 | if (value == null) { 19 | throw new IllegalArgumentException("The identity is required "); 20 | } 21 | this.validateValue(value); 22 | this.value = value; 23 | } 24 | 25 | protected void validateValue(T value) { 26 | // validate value of Identity if need 27 | } 28 | 29 | @Override 30 | public boolean equals(Object o) { 31 | if (this == o) return true; 32 | if (o == null || getClass() != o.getClass()) return false; 33 | AbstractIdentity that = (AbstractIdentity) o; 34 | return value.equals(that.value); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(value); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return this.getClass().getSimpleName() + " [id=" + value + "]"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/domain/Identity.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.domain; 2 | 3 | public interface Identity { 4 | T value(); 5 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/event/ApplicationEvent.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.event; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.UUID; 5 | 6 | public abstract class ApplicationEvent implements Event { 7 | protected final String eventId; 8 | protected final String occurredOn; 9 | protected final String version; 10 | 11 | public ApplicationEvent() { 12 | this("v1.0"); 13 | } 14 | 15 | public ApplicationEvent(String version) { 16 | eventId = UUID.randomUUID().toString(); 17 | occurredOn = LocalDateTime.now().toString(); 18 | this.version = version; 19 | } 20 | 21 | @Override 22 | public String eventId() { 23 | return eventId; 24 | } 25 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/event/Event.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.event; 2 | 3 | import java.io.Serializable; 4 | 5 | public interface Event extends Serializable { 6 | String eventId(); 7 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/exception/ApplicationDomainException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.exception; 2 | 3 | public class ApplicationDomainException extends ApplicationException { 4 | public ApplicationDomainException(String message, Exception ex) { 5 | super(message, ex); 6 | } 7 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/exception/ApplicationException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.exception; 2 | 3 | public abstract class ApplicationException extends RuntimeException { 4 | public ApplicationException(String message) { 5 | super(message); 6 | } 7 | 8 | public ApplicationException(String message, Exception ex) { 9 | super(message, ex); 10 | } 11 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/exception/ApplicationInfrastructureException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.exception; 2 | 3 | public class ApplicationInfrastructureException extends ApplicationException { 4 | public ApplicationInfrastructureException(String message, Exception ex) { 5 | super(message, ex); 6 | } 7 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/exception/ApplicationValidationException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.exception; 2 | 3 | public class ApplicationValidationException extends ApplicationException { 4 | public ApplicationValidationException(String message) { 5 | super(message); 6 | } 7 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/exception/DomainException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.exception; 2 | 3 | public class DomainException extends RuntimeException { 4 | public DomainException(String message) { 5 | super(message); 6 | } 7 | 8 | public DomainException(String message, Throwable cause) { 9 | super(message, cause); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/gateway/north/Resources.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.gateway.north; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | import xyz.zhangyi.ddd.core.exception.ApplicationDomainException; 6 | import xyz.zhangyi.ddd.core.exception.ApplicationInfrastructureException; 7 | import xyz.zhangyi.ddd.core.exception.ApplicationValidationException; 8 | 9 | import java.util.function.Supplier; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | 13 | public class Resources { 14 | private static Logger logger = Logger.getLogger(Resources.class.getName()); 15 | 16 | private Resources(String requestType) { 17 | this.requestType = requestType; 18 | } 19 | 20 | private String requestType; 21 | private HttpStatus successfulStatus; 22 | private HttpStatus errorStatus; 23 | private HttpStatus failedStatus; 24 | 25 | public static Resources with(String requestType) { 26 | return new Resources(requestType); 27 | } 28 | 29 | public Resources onSuccess(HttpStatus status) { 30 | this.successfulStatus = status; 31 | return this; 32 | } 33 | 34 | public Resources onError(HttpStatus status) { 35 | this.errorStatus = status; 36 | return this; 37 | } 38 | 39 | public Resources onFailed(HttpStatus status) { 40 | this.failedStatus = status; 41 | return this; 42 | } 43 | 44 | public ResponseEntity execute(Supplier supplier) { 45 | try { 46 | T entity = supplier.get(); 47 | return new ResponseEntity<>(entity, successfulStatus); 48 | } catch (ApplicationValidationException ex) { 49 | logger.log(Level.WARNING, String.format("The request of %s is invalid", requestType)); 50 | return new ResponseEntity<>(errorStatus); 51 | } catch (ApplicationDomainException ex) { 52 | logger.log(Level.WARNING, String.format("Exception raised %s REST Call", requestType)); 53 | return new ResponseEntity<>(failedStatus); 54 | } catch (ApplicationInfrastructureException ex) { 55 | logger.log(Level.SEVERE, String.format("Fatal exception raised %s REST Call", requestType)); 56 | return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); 57 | } 58 | } 59 | 60 | public ResponseEntity execute(Runnable runnable) { 61 | try { 62 | runnable.run(); 63 | return new ResponseEntity<>(successfulStatus); 64 | } catch (ApplicationValidationException ex) { 65 | logger.log(Level.WARNING, String.format("The request of %s is invalid", requestType)); 66 | return new ResponseEntity<>(errorStatus); 67 | } catch (ApplicationDomainException ex) { 68 | logger.log(Level.WARNING, String.format("Exception raised %s REST Call", requestType)); 69 | return new ResponseEntity<>(failedStatus); 70 | } catch (ApplicationInfrastructureException ex) { 71 | logger.log(Level.SEVERE, String.format("Fatal exception raised %s REST Call", requestType)); 72 | return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/gateway/south/adapter/EventKafkaPublisher.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.gateway.south.adapter; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.apache.kafka.clients.producer.KafkaProducer; 5 | import org.apache.kafka.clients.producer.Producer; 6 | import org.apache.kafka.clients.producer.ProducerRecord; 7 | import xyz.zhangyi.ddd.core.gateway.south.port.Destination; 8 | import xyz.zhangyi.ddd.core.gateway.south.port.EventPublisher; 9 | import xyz.zhangyi.ddd.core.event.Event; 10 | 11 | import java.util.Properties; 12 | 13 | public class EventKafkaPublisher implements EventPublisher { 14 | private Destination destination; 15 | private Producer producer; 16 | 17 | public EventKafkaPublisher(Destination destination) { 18 | this.destination = destination; 19 | } 20 | 21 | @Override 22 | public void publish(Event event) { 23 | producer = createProducer(); 24 | ProducerRecord producerRecord = new ProducerRecord<>(destination.topic(), JSON.toJSONString(event)); 25 | producer.send(producerRecord); 26 | } 27 | 28 | protected Producer createProducer() { 29 | Properties props = buildProperties(destination); 30 | return new KafkaProducer<>(props); 31 | } 32 | 33 | private Properties buildProperties(Destination destination) { 34 | Properties props = new Properties(); 35 | props.put("bootstrap.servers", destination.server()); 36 | props.put("acks", "all"); 37 | props.put("retries", 0); 38 | props.put("batch.size", 16384); 39 | props.put("linger.ms", 1); 40 | props.put("buffer.memory", 33554432); 41 | return props; 42 | } 43 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/gateway/south/port/Destination.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.gateway.south.port; 2 | 3 | public class Destination { 4 | private String host; 5 | private int port; 6 | private String topic; 7 | 8 | public Destination(String host, int port, String topic) { 9 | this.host = host; 10 | this.port = port; 11 | this.topic = topic; 12 | } 13 | 14 | public String server() { 15 | return String.format("%s:%s", host, port); 16 | } 17 | 18 | public String topic() { 19 | return topic; 20 | } 21 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/gateway/south/port/EventPublisher.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.gateway.south.port; 2 | 3 | import xyz.zhangyi.ddd.core.event.Event; 4 | 5 | public interface EventPublisher { 6 | void publish(T event); 7 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/Adapter.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface Adapter { 11 | PortType value(); 12 | } 13 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/Aggregate.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface Aggregate { 11 | } 12 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/BoundedContext.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.PACKAGE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface BoundedContext { 11 | public String name(); 12 | public SubDomain subDomain() default SubDomain.Core; 13 | } -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/Direction.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | public enum Direction { 4 | North, 5 | South 6 | } 7 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/DomainService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface DomainService { 11 | } 12 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/Local.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface Local { 11 | } 12 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/MessageContract.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | 4 | public @interface MessageContract { 5 | Direction value(); 6 | } 7 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/Port.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface Port { 11 | PortType value(); 12 | } 13 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/PortType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | public enum PortType { 4 | Repository, 5 | Client, 6 | Publisher 7 | } 8 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/Remote.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface Remote { 11 | RemoteType value(); 12 | } 13 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/RemoteType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | public enum RemoteType { 4 | Resource, 5 | Controller, 6 | Provider, 7 | Subscriber 8 | } 9 | -------------------------------------------------------------------------------- /ddd-core/src/main/java/xyz/zhangyi/ddd/core/stereotype/SubDomain.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.stereotype; 2 | 3 | public enum SubDomain { 4 | Core, Generic, Supporting 5 | } -------------------------------------------------------------------------------- /ddd-core/src/test/java/xyz/zhangyi/ddd/core/gateway/south/adapter/EventKafkaPublisherTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.core.gateway.south.adapter; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import org.apache.kafka.clients.producer.MockProducer; 5 | import org.apache.kafka.clients.producer.Producer; 6 | import org.apache.kafka.clients.producer.ProducerRecord; 7 | import org.apache.kafka.common.serialization.StringSerializer; 8 | import org.junit.Test; 9 | import xyz.zhangyi.ddd.core.event.ApplicationEvent; 10 | import xyz.zhangyi.ddd.core.gateway.south.port.Destination; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | public class EventKafkaPublisherTest { 18 | private final MockProducer mockProducer = new MockProducer<>(true, new StringSerializer(), new StringSerializer()); 19 | 20 | private class CandidateNominated extends ApplicationEvent { 21 | private String notification; 22 | private String candidateId; 23 | private String nominatorId; 24 | 25 | private CandidateNominated(String notification, String candidateId, String nominatorId) { 26 | this.notification = notification; 27 | this.candidateId = candidateId; 28 | this.nominatorId = nominatorId; 29 | } 30 | } 31 | 32 | private class EventKafkaMockPublisher extends EventKafkaPublisher { 33 | public EventKafkaMockPublisher(Destination destination) { 34 | super(destination); 35 | } 36 | 37 | @Override 38 | protected Producer createProducer() { 39 | return mockProducer; 40 | } 41 | } 42 | 43 | @Test 44 | public void should_send_CandidateNominated_application_events() { 45 | Destination destination = new Destination("localhost", 9012, "test"); 46 | EventKafkaMockPublisher mockPublisher = new EventKafkaMockPublisher(destination); 47 | CandidateNominated candidateNominated1 = new CandidateNominated("nominate ticket", "201912101000", "201001010110"); 48 | CandidateNominated candidateNominated2 = new CandidateNominated("nominate ticket again", "201912101000", "201001010110"); 49 | 50 | mockPublisher.publish(candidateNominated1); 51 | mockPublisher.publish(candidateNominated2); 52 | 53 | List> actualHistory = mockProducer.history(); 54 | List> expectedHistory = Arrays.asList( 55 | new ProducerRecord<>("test", JSON.toJSONString(candidateNominated1)), 56 | new ProducerRecord<>("test", JSON.toJSONString(candidateNominated2)) 57 | ); 58 | assertThat(actualHistory).isEqualTo(expectedHistory); 59 | } 60 | } -------------------------------------------------------------------------------- /eas-employee/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | xyz.zhangyi.ddd 9 | eas 10 | 1.0-SNAPSHOT 11 | 12 | 13 | eas-employee 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | 24 | 25 | xyz.zhangyi.ddd 26 | ddd-core 27 | ${project.parent.version} 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-maven-plugin 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/Attendance.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidAttendanceException; 5 | 6 | import java.time.LocalDate; 7 | 8 | @Aggregate 9 | public class Attendance { 10 | private AttendanceStatus status; 11 | private String employeeId; 12 | private LocalDate workDay; 13 | 14 | public Attendance(String employeeId, LocalDate workDay) { 15 | this.employeeId = employeeId; 16 | this.workDay = workDay; 17 | } 18 | 19 | public void assureStatus(boolean isHoliday, TimeCard timeCard, Leave leave) { 20 | status = withCondition(isHoliday, timeCard, leave).toStatus(); 21 | } 22 | 23 | private Condition withCondition(boolean isHoliday, TimeCard timeCard, Leave leave) { 24 | if (timeCard != null && !timeCard.sameWorkDay(workDay)) { 25 | throw new InvalidAttendanceException("different work day for attendance, time card and leave"); 26 | } 27 | if (leave != null && !leave.sameDay(workDay)) { 28 | throw new InvalidAttendanceException("different work day for attendance, time card and leave"); 29 | } 30 | 31 | return new Condition(isHoliday, timeCard, leave); 32 | } 33 | 34 | public AttendanceStatus status() { 35 | return status; 36 | } 37 | 38 | private static class Condition { 39 | private final boolean isHoliday; 40 | private final TimeCard timeCard; 41 | private final Leave leave; 42 | 43 | private Condition(boolean isHoliday, TimeCard timeCard, Leave leave) { 44 | this.isHoliday = isHoliday; 45 | this.timeCard = timeCard; 46 | this.leave = leave; 47 | } 48 | 49 | private boolean beHoliday() { 50 | return isHoliday && (timeCard == null || !timeCard.isValid()); 51 | } 52 | 53 | private boolean beOvertime() { 54 | return isHoliday && timeCard.isValid(); 55 | } 56 | 57 | private boolean beAbsence() { 58 | return !isHoliday && timeCard == null && leave == null; 59 | } 60 | 61 | private boolean beLeave() { 62 | return !isHoliday && timeCard == null; 63 | } 64 | 65 | private boolean beLateAndLeaveEarly() { 66 | return timeCard.isLate() && timeCard.isLeaveEarly(); 67 | } 68 | 69 | private boolean beLate() { 70 | return timeCard.isLate(); 71 | } 72 | 73 | private boolean beLeaveEarly() { 74 | return timeCard.isLeaveEarly(); 75 | } 76 | 77 | private AttendanceStatus toStatus() { 78 | if (beHoliday()) { 79 | return AttendanceStatus.Holiday; 80 | } 81 | if (beOvertime()) { 82 | return AttendanceStatus.Overtime; 83 | } 84 | if (beAbsence()) { 85 | return AttendanceStatus.Absence; 86 | } 87 | if (beLeave()) { 88 | return leave.attendanceStatus(); 89 | } 90 | if (beLateAndLeaveEarly()) { 91 | return AttendanceStatus.LateAndLeaveEarly; 92 | } 93 | if (beLate()) { 94 | return AttendanceStatus.Late; 95 | } 96 | if (beLeaveEarly()) { 97 | return AttendanceStatus.LeaveEarly; 98 | } 99 | return AttendanceStatus.Normal; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/AttendanceStatus.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | public enum AttendanceStatus { 4 | Normal, 5 | Late, 6 | LeaveEarly, 7 | LateAndLeaveEarly, 8 | Absence, 9 | Holiday, 10 | Overtime, 11 | Leave, 12 | SickLeave, 13 | CasualLeave, 14 | MaternityLeave, 15 | BereavementLeave, 16 | MarriageLeave 17 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/Leave.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class Leave { 6 | private String employeeId; 7 | private LocalDate askLeaveDay; 8 | private LeaveType leaveType; 9 | 10 | private Leave(String employeeId, LocalDate askLeaveDay, LeaveType leaveType) { 11 | this.employeeId = employeeId; 12 | this.askLeaveDay = askLeaveDay; 13 | this.leaveType = leaveType; 14 | } 15 | 16 | public static Leave of(String employeeId, LocalDate askLeaveDay, LeaveType leaveType) { 17 | return new Leave(employeeId, askLeaveDay, leaveType); 18 | } 19 | 20 | public AttendanceStatus attendanceStatus() { 21 | return leaveType.toAttendanceStatus(); 22 | } 23 | 24 | public boolean sameDay(LocalDate workDay) { 25 | return askLeaveDay.isEqual(workDay); 26 | } 27 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/LeaveType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | public enum LeaveType { 4 | Sick, Casual, Maternity, Bereavement, Marriage, Other; 5 | 6 | public AttendanceStatus toAttendanceStatus() { 7 | switch (this) { 8 | case Sick: return AttendanceStatus.SickLeave; 9 | case Casual: return AttendanceStatus.CasualLeave; 10 | case Maternity: return AttendanceStatus.MaternityLeave; 11 | case Bereavement: return AttendanceStatus.BereavementLeave; 12 | case Marriage: return AttendanceStatus.MarriageLeave; 13 | default: return AttendanceStatus.Leave; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/TimeCard.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalTime; 5 | 6 | public class TimeCard { 7 | private LocalDate workDay; 8 | private LocalTime startWork; 9 | private LocalTime endWork; 10 | private WorkTimeRule workTimeRule; 11 | 12 | private TimeCard(LocalDate workDay, 13 | LocalTime startWork, 14 | LocalTime endWork, 15 | WorkTimeRule workTimeRule) { 16 | this.workDay = workDay; 17 | this.startWork = startWork; 18 | this.endWork = endWork; 19 | this.workTimeRule = workTimeRule; 20 | } 21 | 22 | public static TimeCard of(LocalDate workDay, 23 | LocalTime startWork, 24 | LocalTime endWork, 25 | WorkTimeRule workTimeRule) { 26 | return new TimeCard(workDay, startWork, endWork, workTimeRule); 27 | } 28 | 29 | public boolean isLate() { 30 | return workTimeRule.isLate(startWork); 31 | } 32 | 33 | public boolean isLeaveEarly() { 34 | return workTimeRule.isLeaveEarly(endWork); 35 | } 36 | 37 | public boolean isValid() { 38 | return Math.abs(endWork.getHour() - startWork.getHour()) >= 4; 39 | } 40 | 41 | public boolean sameWorkDay(LocalDate workDay) { 42 | return this.workDay.isEqual(workDay); 43 | } 44 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/WorkTimeRule.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | import java.time.LocalTime; 4 | 5 | public class WorkTimeRule { 6 | private LocalTime startWork; 7 | private LocalTime endWork; 8 | private int allowableLateMinutes; 9 | private int allowableLeaveEarlyMinutes; 10 | 11 | private WorkTimeRule(LocalTime startWork, LocalTime endWork, int allowableLateMinutes, int allowableLeaveEarlyMinutes) { 12 | this.startWork = startWork; 13 | this.endWork = endWork; 14 | this.allowableLateMinutes = allowableLateMinutes; 15 | this.allowableLeaveEarlyMinutes = allowableLeaveEarlyMinutes; 16 | } 17 | 18 | public static WorkTimeRule of(LocalTime startWork, LocalTime endWork, int allowableLateMinutes, int allowableLeaveEarlyMinutes) { 19 | return new WorkTimeRule(startWork, endWork, allowableLateMinutes, allowableLeaveEarlyMinutes); 20 | } 21 | 22 | public boolean isLate(LocalTime punchedTime) { 23 | return punchedTime.isAfter(startWork.plusMinutes(allowableLateMinutes)); 24 | } 25 | 26 | public boolean isLeaveEarly(LocalTime punchedTime) { 27 | return punchedTime.isBefore(endWork.minusMinutes(allowableLeaveEarlyMinutes)); 28 | } 29 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/Employee.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import com.google.common.base.Strings; 4 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidEmployeeException; 6 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidEmployeeIdException; 7 | 8 | import java.time.LocalDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.Objects; 11 | 12 | @Aggregate 13 | public class Employee { 14 | private static final int MAX_SEQUENCE_NO = 9999; 15 | private EmployeeId id; 16 | private final String name; 17 | private final IDCard idCard; 18 | private final Phone mobile; 19 | private final Gender gender; 20 | private final LocalDateTime onBoardingDate; 21 | 22 | public Employee(String name, IDCard idCard, Phone mobile) { 23 | this(name, idCard, mobile, LocalDateTime.now()); 24 | } 25 | 26 | public Employee(String name, IDCard idCard, Phone mobile, LocalDateTime onBoardingDate) { 27 | this(null, name, idCard, mobile, onBoardingDate); 28 | } 29 | 30 | public Employee(EmployeeId id, String name, IDCard idCard, Phone mobile, LocalDateTime onBoardingDate) { 31 | this.id = id; 32 | this.name = validateName(name); 33 | this.idCard = requireNonNull(idCard, "ID Card should not be null"); 34 | this.mobile = requireNonNull(mobile, "Mobile Phone should not be null"); 35 | this.gender = idCard.isMale() ? Gender.Male : Gender.Female; 36 | this.onBoardingDate = onBoardingDate; 37 | } 38 | 39 | public EmployeeId id() { 40 | return this.id; 41 | } 42 | 43 | public boolean isMale() { 44 | return gender.isMale(); 45 | } 46 | 47 | public boolean isFemale() { 48 | return gender.isFemale(); 49 | } 50 | 51 | public LocalDateTime onBoardingDate() { 52 | return this.onBoardingDate; 53 | } 54 | 55 | private String validateName(String name) { 56 | if (Strings.isNullOrEmpty(name)) { 57 | throw new InvalidEmployeeException("Name should not be null or empty"); 58 | } 59 | return name; 60 | } 61 | 62 | private T requireNonNull(T obj, String errorMessage) { 63 | if (Objects.isNull(obj)) { 64 | throw new InvalidEmployeeException(errorMessage); 65 | } 66 | return obj; 67 | } 68 | 69 | public synchronized void assignIdFrom(String sequenceCode) { 70 | if (Strings.isNullOrEmpty(sequenceCode)) { 71 | throw new InvalidEmployeeIdException("Invalid sequence code."); 72 | } 73 | 74 | int sequenceNumber = parseSequenceNumber(sequenceCode) + 1; 75 | if (sequenceNumber > MAX_SEQUENCE_NO) { 76 | throw new InvalidEmployeeIdException("Invalid max value of sequence code."); 77 | } 78 | 79 | String currentSequenceCode = Strings.padStart(String.valueOf(sequenceNumber), 4, '0'); 80 | String onBoardingDateCode = onBoardingDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")); 81 | 82 | id = new EmployeeId(String.format("%s%s", onBoardingDateCode, currentSequenceCode)); 83 | } 84 | 85 | private int parseSequenceNumber(String sequenceCode) { 86 | int sequenceNumber; 87 | try { 88 | sequenceNumber = Integer.parseInt(sequenceCode); 89 | } catch (NumberFormatException ex) { 90 | throw new InvalidEmployeeIdException("Invalid sequence code."); 91 | } 92 | return sequenceNumber; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/EmployeeId.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import com.google.common.base.Strings; 4 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidEmployeeIdException; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.utils.DateTimes; 6 | 7 | import java.time.LocalDate; 8 | import java.util.Objects; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class EmployeeId { 13 | private final String id; 14 | 15 | EmployeeId(String id) { 16 | validate(id); 17 | this.id = id; 18 | } 19 | 20 | public static EmployeeId from(String id) { 21 | return new EmployeeId(id); 22 | } 23 | 24 | private void validate(String id) { 25 | validateNullOrEmpty(id); 26 | validateLength(id); 27 | validateOnBoardingDate(id); 28 | validateSequenceCode(id); 29 | } 30 | 31 | private void validateNullOrEmpty(String id) { 32 | if (Strings.isNullOrEmpty(id)) { 33 | throw new InvalidEmployeeIdException("Employee id should not be null or empty."); 34 | } 35 | } 36 | 37 | private void validateLength(String id) { 38 | if (id.length() != 12) { 39 | throw new InvalidEmployeeIdException("The length of employee id should be 12."); 40 | } 41 | } 42 | 43 | private void validateOnBoardingDate(String id) { 44 | String onBoardingDate = id.substring(0, 8); 45 | LocalDate minDate = LocalDate.of(1989, 12, 31); 46 | LocalDate maxDate = LocalDate.now(); 47 | if (!DateTimes.isValidFormat(onBoardingDate, minDate, maxDate)) { 48 | throw new InvalidEmployeeIdException("On boarding date of employee id is invalid."); 49 | } 50 | } 51 | 52 | private void validateSequenceCode(String id) { 53 | String sequenceCode = id.substring(8); 54 | Pattern pattern = Pattern.compile("^([0-9]{4})$"); 55 | Matcher matcher = pattern.matcher(sequenceCode); 56 | if (!matcher.matches()) { 57 | throw new InvalidEmployeeIdException("Sequence code of employee id is invalid."); 58 | } 59 | } 60 | 61 | public String sequenceNo() { 62 | return id.substring(id.length() - 4); 63 | } 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | if (this == o) return true; 68 | if (o == null || getClass() != o.getClass()) return false; 69 | EmployeeId that = (EmployeeId) o; 70 | return id.equals(that.id); 71 | } 72 | 73 | @Override 74 | public int hashCode() { 75 | return Objects.hash(id); 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return String.format("Employee Id is %s", id); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/EmployeeIdGenerator.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.south.port.repository.EmployeeRepository; 4 | 5 | public class EmployeeIdGenerator { 6 | public static final String START_SEQUENCE_NO = "0000"; 7 | private EmployeeRepository empRepo; 8 | 9 | public void generate(Employee employee) { 10 | String sequenceNo = empRepo.latestEmployee().map(e -> e.id().sequenceNo()).orElse(START_SEQUENCE_NO); 11 | employee.assignIdFrom(sequenceNo); 12 | } 13 | 14 | public void setEmployeeRepository(EmployeeRepository empRepo) { 15 | this.empRepo = empRepo; 16 | } 17 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/Gender.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | public enum Gender { 4 | Male, Female; 5 | 6 | public boolean isMale() { 7 | return this == Male; 8 | } 9 | 10 | public boolean isFemale() { 11 | return this == Female; 12 | } 13 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/IDCard.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import com.google.common.base.Strings; 4 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidIdCardException; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.utils.DateTimes; 6 | 7 | import java.time.LocalDate; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | public class IDCard { 12 | private String number; 13 | private static final int LENGTH_OF_ID_CARD = 18; 14 | 15 | public IDCard(String number) { 16 | IDCardValidator idCardValidator = new IDCardValidator(number); 17 | idCardValidator.validate(); 18 | 19 | this.number = number; 20 | } 21 | 22 | public String number() { 23 | return this.number; 24 | } 25 | 26 | public boolean isMale() { 27 | return getGenderCode() % 2 == 1; 28 | } 29 | 30 | public boolean isFemale() { 31 | return getGenderCode() % 2 == 0; 32 | } 33 | 34 | private int getGenderCode() { 35 | char genderFlag = number.charAt(LENGTH_OF_ID_CARD - 2); 36 | return Character.getNumericValue(genderFlag); 37 | } 38 | 39 | public static class IDCardValidator { 40 | private String number; 41 | 42 | IDCardValidator(String number) { 43 | this.number = number; 44 | } 45 | 46 | private void validate() { 47 | validateNullOrEmpty(); 48 | validateLength(); 49 | validateDigitalBits(); 50 | validateBirthday(); 51 | validateChecksum(); 52 | } 53 | 54 | private void validateNullOrEmpty() { 55 | if (Strings.isNullOrEmpty(this.number)) { 56 | throw new InvalidIdCardException("Id card number should not be null or empty."); 57 | } 58 | } 59 | 60 | private void validateLength() { 61 | if (this.number.length() != LENGTH_OF_ID_CARD) { 62 | throw new InvalidIdCardException(String.format("Length of %s is not 18.", this.number)); 63 | } 64 | } 65 | 66 | private void validateDigitalBits() { 67 | Pattern pattern = Pattern.compile("^[1-9]([0-9]{16})[xX0-9]$"); 68 | Matcher matcher = pattern.matcher(this.number); 69 | if (!matcher.matches()) { 70 | throw new InvalidIdCardException(String.format("%s is not begin with digit number.", this.number)); 71 | } 72 | } 73 | 74 | private void validateBirthday() { 75 | String birthdayPart = this.number.substring(6, 14); 76 | LocalDate minDate = LocalDate.of(1900, 1, 1); 77 | LocalDate maxDate = LocalDate.now(); 78 | validateBirthday(birthdayPart, minDate, maxDate); 79 | } 80 | 81 | private void validateBirthday(String birthdayPart, LocalDate minDate, LocalDate maxDate) { 82 | if (!DateTimes.isValidFormat(birthdayPart, minDate, maxDate)) { 83 | throw new InvalidIdCardException("Birthday of Id card is invalid."); 84 | } 85 | } 86 | 87 | private void validateChecksum() { 88 | if (calculateChecksum() != this.number.charAt(LENGTH_OF_ID_CARD - 1)) { 89 | throw new InvalidIdCardException(String.format("Checksum of %s is wrong", this.number)); 90 | } 91 | } 92 | 93 | private char calculateChecksum() { 94 | char[] checksum = {'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'}; 95 | int[] weightOfChecksum = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; 96 | int sum = 0; 97 | for (int i = 0; i < this.number.length() - 1; i++) { 98 | char current = this.number.charAt(i); 99 | sum += (current - '0') * weightOfChecksum[i]; 100 | } 101 | return checksum[sum % 11]; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/Phone.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidPhoneNumberException; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class Phone { 9 | private String number; 10 | 11 | public String number() { 12 | return this.number; 13 | } 14 | 15 | public Phone(String number) { 16 | validate(number); 17 | this.number = number; 18 | } 19 | 20 | private void validate(String number) { 21 | Pattern pattern = Pattern.compile("^[1][3,4,5,7,8][0-9]{9}$"); 22 | Matcher matcher = pattern.matcher(number); 23 | if (!matcher.matches()) { 24 | throw new InvalidPhoneNumberException(String.format("%s is invalid phone number.", number)); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/exception/InvalidAttendanceException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class InvalidAttendanceException extends DomainException { 6 | public InvalidAttendanceException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/exception/InvalidEmployeeException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class InvalidEmployeeException extends DomainException { 6 | public InvalidEmployeeException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/exception/InvalidEmployeeIdException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class InvalidEmployeeIdException extends DomainException { 6 | public InvalidEmployeeIdException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/exception/InvalidIdCardException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class InvalidIdCardException extends DomainException { 6 | public InvalidIdCardException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/exception/InvalidPhoneNumberException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class InvalidPhoneNumberException extends DomainException { 6 | public InvalidPhoneNumberException(String message) { 7 | super(message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/south/port/repository/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.south.port.repository; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Port; 4 | import xyz.zhangyi.ddd.core.stereotype.PortType; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.Employee; 6 | 7 | import java.util.Optional; 8 | 9 | @Port(PortType.Repository) 10 | public interface EmployeeRepository { 11 | Optional latestEmployee(); 12 | } -------------------------------------------------------------------------------- /eas-employee/src/main/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/utils/DateTimes.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.utils; 2 | 3 | import java.time.LocalDate; 4 | import java.time.format.DateTimeFormatter; 5 | import java.time.format.DateTimeParseException; 6 | 7 | public class DateTimes { 8 | public static boolean isValidFormat(String dateString, LocalDate minDate, LocalDate maxDate) { 9 | DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); 10 | try { 11 | LocalDate date = LocalDate.parse(dateString, dateFormatter); 12 | return date.isAfter(minDate) && (date.isBefore(maxDate) || date.isEqual(maxDate)); 13 | } catch (DateTimeParseException ex) { 14 | return false; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /eas-employee/src/test/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/attendance/AttendanceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.attendance; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidAttendanceException; 6 | 7 | import java.time.LocalDate; 8 | import java.time.LocalTime; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | public class AttendanceTest { 14 | private LocalDate workDay; 15 | private String employeeId; 16 | private LocalTime startWork; 17 | private LocalTime endWork; 18 | private int allowableLateMinutes; 19 | private int allowableLeaveEarlyMinutes; 20 | private WorkTimeRule workTimeRule; 21 | private boolean beHoliday; 22 | private boolean notHoliday; 23 | 24 | @Before 25 | public void setUp() { 26 | employeeId = "201801010100"; 27 | workDay = LocalDate.of(2019, 12, 22); 28 | startWork = LocalTime.of(9, 0, 0); 29 | endWork = LocalTime.of(18, 0, 0); 30 | allowableLateMinutes = 15; 31 | allowableLeaveEarlyMinutes = 15; 32 | workTimeRule = WorkTimeRule.of(startWork, endWork, allowableLateMinutes, allowableLeaveEarlyMinutes); 33 | beHoliday = true; 34 | notHoliday = false; 35 | } 36 | 37 | @Test 38 | public void should_be_HOLIDAY_on_holiday_without_time_card() { 39 | // given 40 | Attendance attendance = new Attendance(employeeId, workDay); 41 | 42 | // when 43 | attendance.assureStatus(beHoliday, null, null); 44 | 45 | // then 46 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.Holiday); 47 | } 48 | 49 | @Test 50 | public void should_be_HOLIDAY_on_holiday_with_invalid_time_card() { 51 | // given 52 | LocalTime punchedStartWork = LocalTime.of(9, 00); 53 | LocalTime punchedEndWork = LocalTime.of(12, 59); 54 | TimeCard timeCard = TimeCard.of(workDay, punchedStartWork, punchedEndWork, workTimeRule); 55 | Attendance attendance = new Attendance(employeeId, workDay); 56 | 57 | // when 58 | attendance.assureStatus(beHoliday, timeCard, null); 59 | 60 | // then 61 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.Holiday); 62 | } 63 | 64 | @Test 65 | public void should_be_OVERTIME_on_holiday_with_valid_time_card() { 66 | // given 67 | TimeCard timeCard = TimeCard.of(workDay, startWork, endWork, workTimeRule); 68 | Attendance attendance = new Attendance(employeeId, workDay); 69 | 70 | // when 71 | attendance.assureStatus(beHoliday, timeCard, null); 72 | 73 | // then 74 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.Overtime); 75 | } 76 | 77 | @Test 78 | public void should_be_NORMAL_on_workday_with_time_card() { 79 | // given 80 | TimeCard timeCard = TimeCard.of(workDay, startWork, endWork, workTimeRule); 81 | Attendance attendance = new Attendance(employeeId, workDay); 82 | 83 | // when 84 | attendance.assureStatus(notHoliday, timeCard, null); 85 | 86 | // then 87 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.Normal); 88 | } 89 | 90 | @Test 91 | public void should_be_LATE_on_workday_with_time_card_and_be_late_to_start_work() { 92 | // given 93 | LocalTime punchedStartWork = LocalTime.of(9, 16); 94 | TimeCard timeCard = TimeCard.of(workDay, punchedStartWork, endWork, workTimeRule); 95 | Attendance attendance = new Attendance(employeeId, workDay); 96 | 97 | // when 98 | attendance.assureStatus(notHoliday, timeCard, null); 99 | 100 | // then 101 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.Late); 102 | } 103 | 104 | @Test 105 | public void should_be_LEAVE_EARLY_on_workday_with_time_card_and_be_earlier_than_end_work() { 106 | // given 107 | LocalTime punchedEndWork = LocalTime.of(5, 44); 108 | TimeCard timeCard = TimeCard.of(workDay, startWork, punchedEndWork, workTimeRule); 109 | Attendance attendance = new Attendance(employeeId, workDay); 110 | 111 | // when 112 | attendance.assureStatus(notHoliday, timeCard, null); 113 | 114 | // then 115 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.LeaveEarly); 116 | } 117 | 118 | @Test 119 | public void should_be_LATE_AND_LEAVE_EARLY_on_workday_with_time_card_and_be_late_to_start_work_and_earlier_than_end_work() { 120 | // given 121 | LocalTime punchedStartWork = LocalTime.of(9, 16); 122 | LocalTime punchedEndWork = LocalTime.of(5, 44); 123 | TimeCard timeCard = TimeCard.of(workDay, punchedStartWork, punchedEndWork, workTimeRule); 124 | Attendance attendance = new Attendance(employeeId, workDay); 125 | 126 | // when 127 | attendance.assureStatus(notHoliday, timeCard, null); 128 | 129 | // then 130 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.LateAndLeaveEarly); 131 | } 132 | 133 | @Test 134 | public void should_be_LEAVE_on_workday_without_time_card_and_with_leave() { 135 | // given 136 | LocalDate askLeaveDay = LocalDate.of(2019, 12, 22); 137 | Leave leave = Leave.of(employeeId, askLeaveDay, LeaveType.Sick); 138 | Attendance attendance = new Attendance(employeeId, workDay); 139 | 140 | // when 141 | attendance.assureStatus(notHoliday, null, leave); 142 | 143 | // then 144 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.SickLeave); 145 | } 146 | 147 | @Test 148 | public void should_be_ABSENCE_on_workday_without_time_card_and_leave() { 149 | // given 150 | Attendance attendance = new Attendance(employeeId, workDay); 151 | 152 | // when 153 | attendance.assureStatus(notHoliday, null, null); 154 | 155 | // then 156 | assertThat(attendance.status()).isEqualTo(AttendanceStatus.Absence); 157 | } 158 | 159 | 160 | @Test 161 | public void should_throw_InvalidAttendanceException_given_time_card_with_different_workday() { 162 | LocalDate anotherWorkDay = LocalDate.of(2019, 12, 25); 163 | TimeCard timeCard = TimeCard.of(anotherWorkDay, startWork, endWork, workTimeRule); 164 | Attendance attendance = new Attendance(employeeId, workDay); 165 | 166 | assertThatThrownBy(() -> attendance.assureStatus(notHoliday, timeCard, null)) 167 | .isInstanceOf(InvalidAttendanceException.class) 168 | .hasMessageContaining("different work day for attendance, time card and leave"); 169 | } 170 | 171 | @Test 172 | public void should_throw_InvalidAttendanceException_given_leave_with_different_workday() { 173 | LocalDate anotherWorkDay = LocalDate.of(2019, 12, 25); 174 | Leave leave = Leave.of(employeeId, anotherWorkDay, LeaveType.Sick); 175 | Attendance attendance = new Attendance(employeeId, workDay); 176 | 177 | assertThatThrownBy(() -> attendance.assureStatus(notHoliday, null, leave)) 178 | .isInstanceOf(InvalidAttendanceException.class) 179 | .hasMessageContaining("different work day for attendance, time card and leave"); 180 | } 181 | } -------------------------------------------------------------------------------- /eas-employee/src/test/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/EmployeeIdGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.south.port.repository.EmployeeRepository; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.Optional; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.when; 13 | 14 | public class EmployeeIdGeneratorTest { 15 | 16 | private static final Employee latestEmp = new Employee( 17 | EmployeeId.from("201912240102"), 18 | "Zhang Yi", 19 | new IDCard("34052419800101001X"), 20 | new Phone("13013220101"), 21 | LocalDateTime.now()); 22 | 23 | private static final Employee newEmp = new Employee( 24 | "Zhang Yi New", 25 | new IDCard("34052419800101001X"), 26 | new Phone("13013220101"), 27 | LocalDateTime.of(2020, 4, 2, 22, 10)); 28 | 29 | @Test 30 | public void should_generate_next_employeeId_given_sequence_code() { 31 | EmployeeRepository mockEmpRepo = mock(EmployeeRepository.class); 32 | when(mockEmpRepo.latestEmployee()).thenReturn(Optional.of(latestEmp)); 33 | 34 | EmployeeIdGenerator generator = new EmployeeIdGenerator(); 35 | generator.setEmployeeRepository(mockEmpRepo); 36 | generator.generate(newEmp); 37 | 38 | assertThat(newEmp.id()).isEqualTo(EmployeeId.from("202004020103")); 39 | verify(mockEmpRepo).latestEmployee(); 40 | } 41 | 42 | @Test 43 | public void should_generate_next_employeeId_begin_with_0001_given_no_employee_found() { 44 | EmployeeRepository mockEmpRepo = mock(EmployeeRepository.class); 45 | when(mockEmpRepo.latestEmployee()).thenReturn(Optional.empty()); 46 | 47 | EmployeeIdGenerator generator = new EmployeeIdGenerator(); 48 | generator.setEmployeeRepository(mockEmpRepo); 49 | generator.generate(newEmp); 50 | 51 | assertThat(newEmp.id()).isEqualTo(EmployeeId.from("202004020001")); 52 | verify(mockEmpRepo).latestEmployee(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /eas-employee/src/test/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/EmployeeIdTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.Employee; 6 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.EmployeeId; 7 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.IDCard; 8 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.Phone; 9 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidEmployeeIdException; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | 16 | public class EmployeeIdTest { 17 | private static String validName; 18 | private static IDCard validIdCard; 19 | private static Phone validPhone; 20 | private Employee employee; 21 | 22 | @Before 23 | public void setUp() { 24 | validName = "zhangyi"; 25 | validIdCard = new IDCard("34052419800101001X"); 26 | validPhone = new Phone("13013220101"); 27 | LocalDateTime onBoardingDate = LocalDateTime.of(2019, 12, 24, 10, 0); 28 | employee = new Employee(validName, validIdCard, validPhone, onBoardingDate); 29 | } 30 | 31 | @Test 32 | public void should_generate_employee_id_given_sequence_no() { 33 | employee.assignIdFrom("0101"); 34 | 35 | EmployeeId expected = new EmployeeId("201912240102"); 36 | assertThat(employee.id()).isEqualTo(expected); 37 | assertThat(employee.id().sequenceNo()).isEqualTo("0102"); 38 | } 39 | 40 | @Test 41 | public void should_throw_InvalidEmployeeIdException_given_empty_sequence_no() { 42 | assertThatThrownBy(() -> employee.assignIdFrom("")) 43 | .isInstanceOf(InvalidEmployeeIdException.class) 44 | .hasMessageContaining("Invalid sequence code"); 45 | } 46 | 47 | @Test 48 | public void should_throw_InvalidEmployeeIdException_given_invalid_sequence_no() { 49 | assertThatThrownBy(() -> employee.assignIdFrom("xyz")) 50 | .isInstanceOf(InvalidEmployeeIdException.class) 51 | .hasMessageContaining("Invalid sequence code"); 52 | } 53 | 54 | @Test 55 | public void should_throw_InvalidEmployeeIdException_given_sequence_no_which_greater_than_and_equal_to_9999() { 56 | assertThatThrownBy(() -> employee.assignIdFrom("9999")) 57 | .isInstanceOf(InvalidEmployeeIdException.class) 58 | .hasMessageContaining("Invalid max value of sequence code"); 59 | } 60 | 61 | @Test 62 | public void should_throw_InvalidEmployeeIdException_given_null_id_value() { 63 | assertThatThrownBy(() -> new EmployeeId(null)) 64 | .isInstanceOf(InvalidEmployeeIdException.class) 65 | .hasMessageContaining("Employee id should not be null"); 66 | } 67 | 68 | @Test 69 | public void should_throw_InvalidEmployeeIdException_given_empty_id_value() { 70 | assertThatThrownBy(() -> new EmployeeId("")) 71 | .isInstanceOf(InvalidEmployeeIdException.class) 72 | .hasMessageContaining("Employee id should not be null or empty"); 73 | } 74 | 75 | @Test 76 | public void should_throw_InvalidEmployeeIdException_given_incorrect_length_of_id_value() { 77 | assertThatThrownBy(() -> new EmployeeId("1100110001011")) 78 | .isInstanceOf(InvalidEmployeeIdException.class) 79 | .hasMessageContaining("The length of employee id should be 12"); 80 | } 81 | 82 | @Test 83 | public void should_throw_InvalidEmployeeIdException_given_incorrect_on_boarding_date_of_id_value() { 84 | assertThatThrownBy(() -> new EmployeeId("110011000101")) 85 | .isInstanceOf(InvalidEmployeeIdException.class) 86 | .hasMessageContaining("On boarding date of employee id is invalid"); 87 | } 88 | 89 | @Test 90 | public void should_throw_InvalidEmployeeIdException_given_incorrect_sequence_code_of_id_value() { 91 | assertThatThrownBy(() -> new EmployeeId("20191201111X")) 92 | .isInstanceOf(InvalidEmployeeIdException.class) 93 | .hasMessageContaining("Sequence code of employee id is invalid"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /eas-employee/src/test/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/EmployeeTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.Employee; 6 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.IDCard; 7 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.Phone; 8 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidEmployeeException; 9 | 10 | import java.time.LocalDateTime; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 14 | 15 | public class EmployeeTest { 16 | private static String validName; 17 | private static IDCard validIdCard; 18 | private static Phone validPhone; 19 | 20 | @Before 21 | public void setUp() { 22 | validName = "guo jing"; 23 | validIdCard = new IDCard("34052419800101001X"); 24 | validPhone = new Phone("13013220101"); 25 | } 26 | 27 | @Test 28 | public void should_throw_InvalidEmployeeException_if_name_is_null() { 29 | String name = null; 30 | 31 | assertThatThrownBy(() -> new Employee(name, validIdCard, validPhone)) 32 | .isInstanceOf(InvalidEmployeeException.class) 33 | .hasMessageContaining("Name should not be null or empty"); 34 | } 35 | 36 | @Test 37 | public void should_throw_InvalidEmployeeException_if_name_is_empty() { 38 | String name = ""; 39 | 40 | assertThatThrownBy(() -> new Employee(name, validIdCard, validPhone)) 41 | .isInstanceOf(InvalidEmployeeException.class) 42 | .hasMessageContaining("Name should not be null or empty"); 43 | } 44 | 45 | @Test 46 | public void should_throw_InvalidEmployeeException_if_IdCard_is_null() { 47 | IDCard idCard = null; 48 | 49 | assertThatThrownBy(() -> new Employee(validName, idCard, validPhone)) 50 | .isInstanceOf(InvalidEmployeeException.class) 51 | .hasMessageContaining("ID Card should not be null"); 52 | } 53 | 54 | @Test 55 | public void should_throw_InvalidEmployeeException_if_mobile_phone_is_null() { 56 | Phone mobile = null; 57 | 58 | assertThatThrownBy(() -> new Employee(validName, validIdCard, mobile)) 59 | .isInstanceOf(InvalidEmployeeException.class) 60 | .hasMessageContaining("Mobile Phone should not be null"); 61 | } 62 | 63 | @Test 64 | public void should_set_correct_male_gender_given_correct_id_card() { 65 | Employee employee = new Employee(validName, validIdCard, validPhone); 66 | 67 | assertThat(employee.isMale()).isTrue(); 68 | } 69 | 70 | @Test 71 | public void should_set_correct_female_gender_and_on_boarding_date_given_correct_id_card() { 72 | IDCard femaleIdCard = new IDCard("510225199011015187"); 73 | Employee employee = new Employee(validName, femaleIdCard, validPhone); 74 | 75 | assertThat(employee.isFemale()).isTrue(); 76 | assertThat(employee.onBoardingDate()).isEqualToIgnoringHours(LocalDateTime.now()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /eas-employee/src/test/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/IDCardTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.IDCard; 6 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidIdCardException; 7 | 8 | import java.time.LocalDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 13 | 14 | public class IDCardTest { 15 | private static final String NULL_OR_EMPTY_ERROR_MESSAGE = "Id card number should not be null or empty"; 16 | private static final String DIGIT_NUMBER_ERROR_MESSAGE = "is not begin with digit number"; 17 | private DateTimeFormatter dateFormatter; 18 | private String validIdCardNumberOfMale = "34052419800101001X"; 19 | private String validIdCardNumberOfFemale = "510225199011015187"; 20 | 21 | @Before 22 | public void setUp() { 23 | dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); 24 | } 25 | 26 | @Test 27 | public void should_throw_InvalidIdCardException_given_null_number() { 28 | assertThatThrownBy(() -> new IDCard(null)) 29 | .isInstanceOf(InvalidIdCardException.class) 30 | .hasMessageContaining(NULL_OR_EMPTY_ERROR_MESSAGE); 31 | } 32 | 33 | @Test 34 | public void should_throw_InvalidIdCardException_given_empty_number() { 35 | assertThatThrownBy(() -> new IDCard("")) 36 | .isInstanceOf(InvalidIdCardException.class) 37 | .hasMessageContaining(NULL_OR_EMPTY_ERROR_MESSAGE); 38 | } 39 | 40 | @Test 41 | public void should_throw_InvalidIdCardException_given_number_which_length_is_not_18() { 42 | assertThatThrownBy(() -> new IDCard("1234567890123456789")) 43 | .isInstanceOf(InvalidIdCardException.class) 44 | .hasMessageContaining("is not 18"); 45 | } 46 | 47 | @Test 48 | public void should_throw_InvalidIdCardException_given_number_start_with_non_digital() { 49 | assertThatThrownBy(() -> new IDCard("X12345678901234567")) 50 | .isInstanceOf(InvalidIdCardException.class) 51 | .hasMessageContaining(DIGIT_NUMBER_ERROR_MESSAGE); 52 | } 53 | 54 | @Test 55 | public void should_throw_InvalidIdCardException_given_number_start_with_0() { 56 | assertThatThrownBy(() -> new IDCard("012345678901234567")) 57 | .isInstanceOf(InvalidIdCardException.class) 58 | .hasMessageContaining(DIGIT_NUMBER_ERROR_MESSAGE); 59 | } 60 | 61 | @Test 62 | public void should_throw_InvalidIdCardException_given_number_contains_non_digital_value_start_2_to_17() { 63 | assertThatThrownBy(() -> new IDCard("1X2345678901234567")) 64 | .isInstanceOf(InvalidIdCardException.class) 65 | .hasMessageContaining(DIGIT_NUMBER_ERROR_MESSAGE); 66 | } 67 | 68 | @Test 69 | public void should_throw_InvalidIdCardException_given_number_end_with_other_letter() { 70 | assertThatThrownBy(() -> new IDCard("12345678901234567Y")) 71 | .isInstanceOf(InvalidIdCardException.class) 72 | .hasMessageContaining(DIGIT_NUMBER_ERROR_MESSAGE); 73 | } 74 | 75 | @Test 76 | public void should_throw_InvalidIdCardException_given_number_with_invalid_birthday() { 77 | assertThatThrownBy(() -> new IDCard("510225198013015130")) 78 | .isInstanceOf(InvalidIdCardException.class) 79 | .hasMessageContaining("Birthday of Id card is invalid"); 80 | } 81 | 82 | @Test 83 | public void should_throw_InvalidIdCardException_given_number_with_birthday_after_now() { 84 | LocalDateTime now = LocalDateTime.now().plusDays(1); 85 | String strOfNow = now.format(dateFormatter); 86 | 87 | assertThatThrownBy(() -> new IDCard(String.format("510225%s5130", strOfNow))) 88 | .isInstanceOf(InvalidIdCardException.class) 89 | .hasMessageContaining("Birthday of Id card is invalid"); 90 | } 91 | 92 | @Test 93 | public void should_throw_InvalidIdCardException_given_number_with_birthday_before_1900() { 94 | assertThatThrownBy(() -> new IDCard(String.format("510225%s5130", "18991231"))) 95 | .isInstanceOf(InvalidIdCardException.class) 96 | .hasMessageContaining("Birthday of Id card is invalid"); 97 | } 98 | 99 | @Test 100 | public void should_throw_InvalidIdCardException_given_wrong_checksum() { 101 | assertThatThrownBy(() -> new IDCard("510225199011015131")) 102 | .isInstanceOf(InvalidIdCardException.class) 103 | .hasMessageContaining("Checksum") 104 | .hasMessageContaining("is wrong"); 105 | } 106 | 107 | @Test 108 | public void should_set_number_for_id_card() { 109 | IDCard idCard = new IDCard(validIdCardNumberOfMale); 110 | 111 | assertThat(idCard.number()).isEqualTo(validIdCardNumberOfMale); 112 | } 113 | 114 | @Test 115 | public void should_be_male_given_number_with_odd_value_at_17() { 116 | IDCard idCard = new IDCard(validIdCardNumberOfMale); 117 | 118 | assertThat(idCard.isMale()).isTrue(); 119 | } 120 | 121 | @Test 122 | public void should_be_male_given_number_with_even_value_at_17() { 123 | IDCard idCard = new IDCard(validIdCardNumberOfFemale); 124 | 125 | assertThat(idCard.isFemale()).isTrue(); 126 | } 127 | } -------------------------------------------------------------------------------- /eas-employee/src/test/java/xyz/zhangyi/ddd/eas/valueadded/employeecontext/domain/employee/PhoneTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.employee.Phone; 5 | import xyz.zhangyi.ddd.eas.valueadded.employeecontext.domain.exception.InvalidPhoneNumberException; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 9 | 10 | public class PhoneTest { 11 | @Test 12 | public void should_throw_InvalidPhoneNumberException_given_wrong_phone_number() { 13 | assertThatThrownBy(() -> new Phone("11111111111")) 14 | .isInstanceOf(InvalidPhoneNumberException.class) 15 | .hasMessageContaining("invalid phone number"); 16 | } 17 | 18 | @Test 19 | public void should_set_number_given_correct_phone_number() { 20 | String phoneNumber = "15023157777"; 21 | Phone phone = new Phone(phoneNumber); 22 | 23 | assertThat(phone.number()).isEqualTo(phoneNumber); 24 | } 25 | } -------------------------------------------------------------------------------- /eas-entry/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | xyz.zhangyi.ddd 9 | eas 10 | 1.0-SNAPSHOT 11 | 12 | 13 | eas-entry 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-maven-plugin 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /eas-entry/src/main/java/xyz/zhangyi/ddd/eas/EasApplication.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.transaction.annotation.EnableTransactionManagement; 6 | 7 | @SpringBootApplication 8 | @EnableTransactionManagement 9 | public class EasApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(EasApplication.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /eas-entry/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | mybatis: 2 | mapperLocations: classpath:mapper/*.xml 3 | type-aliases-package: xyz.zhangyi.ddd.eas.trainingcontext.domain 4 | type-handlers-package: xyz.zhangyi.ddd.eas.trainingcontext.gateway.acl.impl.persistence.typehandlers 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:mysql://localhost:3306/eas-db?serverTimezone=UTC 9 | username: sa 10 | password: 123456 11 | driver-class-name: com.mysql.cj.jdbc.Driver 12 | # 使用druid数据源 13 | type: com.alibaba.druid.pool.DruidDataSource -------------------------------------------------------------------------------- /eas-project/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | xyz.zhangyi.ddd 9 | eas 10 | 1.0-SNAPSHOT 11 | 12 | 13 | eas-project 14 | 15 | 16 | 17 | xyz.zhangyi.ddd 18 | ddd-core 19 | ${project.parent.version} 20 | 21 | 22 | -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/changehistory/ChangeHistory.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Aggregate 8 | public class ChangeHistory { 9 | private ChangeHistoryId changeHistoryId; 10 | private Operation operation; 11 | private String issueId; 12 | private Operator operatedBy; 13 | private LocalDateTime operatedAt; 14 | 15 | public ChangeHistory(Operation operation) { 16 | this.changeHistoryId = ChangeHistoryId.next(); 17 | this.operation = operation; 18 | } 19 | 20 | public static ChangeHistory operate(Operation operation) { 21 | return new ChangeHistory(operation); 22 | } 23 | 24 | public String issueId() { 25 | return this.issueId; 26 | } 27 | 28 | public Operator operatedBy() { 29 | return this.operatedBy; 30 | } 31 | 32 | public Operation operation() { 33 | return this.operation; 34 | } 35 | 36 | public LocalDateTime operatedAt() { 37 | return this.operatedAt; 38 | } 39 | 40 | public ChangeHistory to(String issueId) { 41 | this.issueId = issueId; 42 | return this; 43 | } 44 | 45 | public ChangeHistory by(Operator operator) { 46 | this.operatedBy = operator; 47 | return this; 48 | } 49 | 50 | public ChangeHistory at(LocalDateTime operatedTime) { 51 | this.operatedAt = operatedTime; 52 | return this; 53 | } 54 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/changehistory/ChangeHistoryId.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory; 2 | 3 | import java.util.UUID; 4 | 5 | public class ChangeHistoryId { 6 | private final String id; 7 | 8 | private ChangeHistoryId(String id) { 9 | this.id = id; 10 | } 11 | 12 | public static ChangeHistoryId next() { 13 | return new ChangeHistoryId(UUID.randomUUID().toString()); 14 | } 15 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/changehistory/Operation.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory; 2 | 3 | public enum Operation { 4 | Assignment 5 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/changehistory/Operator.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory; 2 | 3 | public class Operator { 4 | private String operatorId; 5 | private String name; 6 | 7 | public Operator(String operatorId, String name) { 8 | this.operatorId = operatorId; 9 | this.name = name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/exception/AssignmentIssueException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class AssignmentIssueException extends DomainException { 6 | public AssignmentIssueException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/exception/IssueException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class IssueException extends DomainException { 6 | public IssueException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/Issue.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.ChangeHistory; 5 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.Operation; 6 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.Operator; 7 | import xyz.zhangyi.ddd.eas.projectcontext.domain.exception.AssignmentIssueException; 8 | import java.time.LocalDateTime; 9 | 10 | @Aggregate 11 | public class Issue { 12 | private IssueId issueId; 13 | private String name; 14 | private String description; 15 | private String ownerId; 16 | private IssueStatus status; 17 | 18 | private Issue(IssueId issueId, String name, String description) { 19 | this.issueId = issueId; 20 | this.name = name; 21 | this.description = description; 22 | this.status = IssueStatus.Open; 23 | } 24 | 25 | public static Issue of(IssueId issueId, String name, String description) { 26 | return new Issue(issueId, name, description); 27 | } 28 | 29 | public ChangeHistory assignTo(IssueOwner owner, Operator operator) { 30 | if (status.isResolved()) { 31 | throw new AssignmentIssueException("resolved issue can not be assigned."); 32 | } 33 | if (status.isClosed()) { 34 | throw new AssignmentIssueException("closed issue can not be assigned."); 35 | } 36 | if (this.ownerId != null && this.ownerId.equals(owner.id())) { 37 | throw new AssignmentIssueException("issue can not be assign to same owner again."); 38 | } 39 | this.ownerId = owner.id(); 40 | return ChangeHistory 41 | .operate(Operation.Assignment) 42 | .to(issueId.id()) 43 | .by(operator) 44 | .at(LocalDateTime.now()); 45 | } 46 | 47 | public String ownerId() { 48 | return ownerId; 49 | } 50 | 51 | public IssueStatus status() { 52 | return status; 53 | } 54 | 55 | public void changeStatusTo(IssueStatus issueStatus) { 56 | this.status = issueStatus; 57 | } 58 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/IssueId.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | public class IssueId { 4 | private String id; 5 | 6 | public IssueId(String id) { 7 | this.id = id; 8 | } 9 | 10 | public String id() { 11 | return null; 12 | } 13 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/IssueOwner.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | public class IssueOwner { 4 | private String ownerId; 5 | private String name; 6 | private String email; 7 | 8 | public IssueOwner(String ownerId, String name, String email) { 9 | this.ownerId = ownerId; 10 | this.name = name; 11 | this.email = email; 12 | } 13 | 14 | public String id() { 15 | return this.ownerId; 16 | } 17 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/IssueService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | import xyz.zhangyi.ddd.eas.projectcontext.south.port.IssueRepository; 4 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.ChangeHistory; 5 | import xyz.zhangyi.ddd.eas.projectcontext.south.port.ChangeHistoryRepository; 6 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.Operator; 7 | import xyz.zhangyi.ddd.eas.projectcontext.domain.exception.IssueException; 8 | 9 | import java.util.Optional; 10 | 11 | public class IssueService { 12 | private IssueRepository issueRepo; 13 | private ChangeHistoryRepository changeHistoryRepo; 14 | 15 | public void assign(IssueId issueId, IssueOwner owner, Operator operator) { 16 | Optional optIssue = issueRepo.issueOf(issueId); 17 | Issue issue = optIssue.orElseThrow(() -> issueNotFoundError(issueId)); 18 | 19 | ChangeHistory changeHistory = issue.assignTo(owner, operator); 20 | 21 | issueRepo.update(issue); 22 | changeHistoryRepo.add(changeHistory); 23 | } 24 | 25 | private IssueException issueNotFoundError(IssueId issueId) { 26 | return new IssueException(String.format("issue with id {%s} was not found", issueId.id())); 27 | } 28 | 29 | public void setIssueRepository(IssueRepository issueRepo) { 30 | this.issueRepo = issueRepo; 31 | } 32 | 33 | public void setChangeHistoryRepository(ChangeHistoryRepository changeHistoryRepo) { 34 | this.changeHistoryRepo = changeHistoryRepo; 35 | } 36 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/IssueStatus.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | public enum IssueStatus { 4 | Resolved, Open, Closed; 5 | 6 | public boolean isResolved() { 7 | return this == Resolved; 8 | } 9 | 10 | public boolean isClosed() { 11 | return this == Closed; 12 | } 13 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/south/port/ChangeHistoryRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.south.port; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Port; 4 | import xyz.zhangyi.ddd.core.stereotype.PortType; 5 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.ChangeHistory; 6 | 7 | @Port(PortType.Repository) 8 | public interface ChangeHistoryRepository { 9 | void add(ChangeHistory changeHistory); 10 | } -------------------------------------------------------------------------------- /eas-project/src/main/java/xyz/zhangyi/ddd/eas/projectcontext/south/port/IssueRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.south.port; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Port; 4 | import xyz.zhangyi.ddd.core.stereotype.PortType; 5 | import xyz.zhangyi.ddd.eas.projectcontext.domain.issue.Issue; 6 | import xyz.zhangyi.ddd.eas.projectcontext.domain.issue.IssueId; 7 | 8 | import java.util.Optional; 9 | 10 | @Port(PortType.Repository) 11 | public interface IssueRepository { 12 | Optional issueOf(IssueId issueId); 13 | void update(Issue issue); 14 | } -------------------------------------------------------------------------------- /eas-project/src/test/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/IssueServiceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.zhangyi.ddd.eas.projectcontext.south.port.IssueRepository; 6 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.ChangeHistory; 7 | import xyz.zhangyi.ddd.eas.projectcontext.south.port.ChangeHistoryRepository; 8 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.Operator; 9 | import xyz.zhangyi.ddd.eas.projectcontext.domain.exception.IssueException; 10 | 11 | import java.util.Optional; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | import static org.mockito.Mockito.*; 16 | 17 | public class IssueServiceTest { 18 | private IssueId issueId; 19 | private Operator operator; 20 | private IssueService issueService; 21 | private IssueOwner owner; 22 | 23 | @Before 24 | public void setUp() { 25 | issueId = new IssueId("#1"); 26 | operator = new Operator("200010100007", "admin"); 27 | issueService = new IssueService(); 28 | owner = new IssueOwner("200901010111", "zhangyi", "zhangyi@eas.com"); 29 | } 30 | 31 | @Test 32 | public void should_assign_issue_to_specific_owner_and_generate_change_history() { 33 | Issue issue = Issue.of(issueId, "test issue", "test desc"); 34 | 35 | IssueRepository issueRepo = mock(IssueRepository.class); 36 | when(issueRepo.issueOf(issueId)).thenReturn(Optional.of(issue)); 37 | issueService.setIssueRepository(issueRepo); 38 | 39 | ChangeHistoryRepository changeHistoryRepo = mock(ChangeHistoryRepository.class); 40 | issueService.setChangeHistoryRepository(changeHistoryRepo); 41 | 42 | issueService.assign(issueId, owner, operator); 43 | 44 | assertThat(issue.ownerId()).isEqualTo(owner.id()); 45 | verify(issueRepo).issueOf(issueId); 46 | verify(issueRepo).update(issue); 47 | verify(changeHistoryRepo).add(isA(ChangeHistory.class)); 48 | } 49 | 50 | @Test 51 | public void should_throw_IssueException_given_no_issue_found_given_issueId() { 52 | IssueRepository issueRepo = mock(IssueRepository.class); 53 | when(issueRepo.issueOf(issueId)).thenReturn(Optional.empty()); 54 | issueService.setIssueRepository(issueRepo); 55 | 56 | ChangeHistoryRepository changeHistoryRepo = mock(ChangeHistoryRepository.class); 57 | issueService.setChangeHistoryRepository(changeHistoryRepo); 58 | 59 | assertThatThrownBy(() -> issueService.assign(issueId, owner, operator)) 60 | .isInstanceOf(IssueException.class) 61 | .hasMessageContaining("issue") 62 | .hasMessageContaining("not found"); 63 | verify(issueRepo).issueOf(issueId); 64 | verify(issueRepo, never()).update(isA(Issue.class)); 65 | verify(changeHistoryRepo, never()).add(isA(ChangeHistory.class)); 66 | } 67 | } -------------------------------------------------------------------------------- /eas-project/src/test/java/xyz/zhangyi/ddd/eas/projectcontext/domain/issue/IssueTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.projectcontext.domain.issue; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import java.time.LocalDateTime; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 8 | 9 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.ChangeHistory; 10 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.Operation; 11 | import xyz.zhangyi.ddd.eas.projectcontext.domain.changehistory.Operator; 12 | import xyz.zhangyi.ddd.eas.projectcontext.domain.exception.AssignmentIssueException; 13 | 14 | public class IssueTest { 15 | private IssueId issueId; 16 | private String name; 17 | private String description; 18 | private IssueOwner owner; 19 | private Operator operator; 20 | 21 | @Before 22 | public void setUp() { 23 | issueId = new IssueId("#1"); 24 | name = "test issue"; 25 | description = "description"; 26 | owner = new IssueOwner("200901010111", "zhangyi", "zhangyi@eas.com"); 27 | operator = new Operator("200001010007", "admin"); 28 | } 29 | 30 | @Test 31 | public void should_be_OPEN_when_new_issue_is_created() { 32 | Issue issue = Issue.of(issueId, name, description); 33 | 34 | assertThat(issue.status()).isEqualTo(IssueStatus.Open); 35 | } 36 | 37 | @Test 38 | public void should_be_changed_to_target_status() { 39 | Issue issue = Issue.of(issueId, name, description); 40 | 41 | assertThat(issue.status()).isEqualTo(IssueStatus.Open); 42 | 43 | issue.changeStatusTo(IssueStatus.Resolved); 44 | 45 | assertThat(issue.status()).isEqualTo(IssueStatus.Resolved); 46 | } 47 | 48 | @Test 49 | public void should_assign_to_specific_owner_and_generate_change_history() { 50 | Issue issue = Issue.of(issueId, name, description); 51 | 52 | ChangeHistory history = issue.assignTo(owner, operator); 53 | 54 | assertThat(issue.ownerId()).isEqualTo(owner.id()); 55 | assertThat(history.issueId()).isEqualTo(issueId.id()); 56 | assertThat(history.operatedBy()).isEqualTo(operator); 57 | assertThat(history.operation()).isEqualTo(Operation.Assignment); 58 | assertThat(history.operatedAt()).isEqualToIgnoringSeconds(LocalDateTime.now()); 59 | } 60 | 61 | @Test 62 | public void should_throw_AssignmentIssueException_when_assign_resolved_issue() { 63 | Issue issue = Issue.of(issueId, name, description); 64 | issue.changeStatusTo(IssueStatus.Resolved); 65 | 66 | assertThatThrownBy(() -> issue.assignTo(owner, operator)) 67 | .isInstanceOf(AssignmentIssueException.class) 68 | .hasMessageContaining("resolved issue can not be assigned"); 69 | } 70 | 71 | @Test 72 | public void should_throw_AssignmentIssueException_when_assign_closed_issue() { 73 | Issue issue = Issue.of(issueId, name, description); 74 | issue.changeStatusTo(IssueStatus.Closed); 75 | 76 | assertThatThrownBy(() -> issue.assignTo(owner, operator)) 77 | .isInstanceOf(AssignmentIssueException.class) 78 | .hasMessageContaining("closed issue can not be assigned"); 79 | } 80 | 81 | @Test 82 | public void should_throw_AssignmentIssueException_when_issue_is_assigned_to_same_owner() { 83 | Issue issue = Issue.of(issueId, name, description); 84 | issue.assignTo(owner, operator); 85 | 86 | assertThatThrownBy(() -> issue.assignTo(owner, operator)) 87 | .isInstanceOf(AssignmentIssueException.class) 88 | .hasMessageContaining("issue can not be assign to same owner again"); 89 | } 90 | } -------------------------------------------------------------------------------- /eas-training/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | xyz.zhangyi.ddd 9 | eas 10 | 1.0-SNAPSHOT 11 | 12 | 13 | eas-training 14 | jar 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter 20 | 21 | 22 | xyz.zhangyi.ddd 23 | ddd-core 24 | ${project.parent.version} 25 | 26 | 27 | org.antlr 28 | ST4 29 | 4.2 30 | 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-maven-plugin 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/candidate/Candidate.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominee; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.TicketOwner; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.TicketOwnerType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 8 | 9 | import java.util.UUID; 10 | 11 | @Aggregate 12 | public class Candidate { 13 | private String id; 14 | private String employeeId; 15 | private String name; 16 | private String email; 17 | private TrainingId trainingId; 18 | 19 | public Candidate(String EmployeeId, String name, String email, TrainingId trainingId) { 20 | this.id = UUID.randomUUID().toString(); 21 | this.employeeId = EmployeeId; 22 | this.name = name; 23 | this.email = email; 24 | this.trainingId = trainingId; 25 | } 26 | 27 | public String employeeId() { 28 | return employeeId; 29 | } 30 | 31 | public String name() { 32 | return this.name; 33 | } 34 | 35 | public String email() { 36 | return email; 37 | } 38 | 39 | public TrainingId trainingId() { 40 | return trainingId; 41 | } 42 | 43 | public TicketOwner toOwner() { 44 | return new TicketOwner(employeeId, TicketOwnerType.Nominee); 45 | } 46 | 47 | public Nominee toNominee() { 48 | return new Nominee(employeeId, name, email); 49 | } 50 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/course/CourseId.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course; 2 | 3 | import xyz.zhangyi.ddd.core.domain.AbstractIdentity; 4 | 5 | import java.util.UUID; 6 | 7 | public class CourseId extends AbstractIdentity { 8 | private String value; 9 | 10 | protected CourseId(String value) { 11 | super(value); 12 | } 13 | 14 | public static CourseId from(String value) { 15 | return new CourseId(value); 16 | } 17 | 18 | public static CourseId next() { 19 | return new CourseId(UUID.randomUUID().toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/exception/NominationException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class NominationException extends DomainException { 6 | public NominationException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/exception/TicketException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class TicketException extends DomainException { 6 | public TicketException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/learning/Learning.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.learning; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 6 | 7 | @Aggregate 8 | public class Learning { 9 | private String learningId; 10 | private CourseId courseId; 11 | private TrainingId trainingId; 12 | private String traineeId; 13 | 14 | public Learning(String learningId, CourseId courseId, TrainingId trainingId, String traineeId) { 15 | this.learningId = learningId; 16 | this.courseId = courseId; 17 | this.traineeId = traineeId; 18 | this.traineeId = traineeId; 19 | } 20 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/learning/LearningService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.learning; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import xyz.zhangyi.ddd.core.stereotype.DomainService; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.LearningRepository; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingException; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TrainingRepository; 11 | 12 | import java.util.Optional; 13 | 14 | @Service 15 | @DomainService 16 | public class LearningService { 17 | @Autowired 18 | private TrainingRepository trainingRepo; 19 | @Autowired 20 | private LearningRepository learningRepo; 21 | 22 | public void setTrainingRepository(TrainingRepository trainingRepo) { 23 | this.trainingRepo = trainingRepo; 24 | } 25 | 26 | public void setLearningRepository(LearningRepository learningRepo) { 27 | this.learningRepo = learningRepo; 28 | } 29 | 30 | public boolean beLearned(String traineeId, TrainingId trainingId) { 31 | Optional optionalTraining = trainingRepo.trainingOf(trainingId); 32 | if (!optionalTraining.isPresent()) 33 | throw new TrainingException(String.format("training by id {%s} can not be found.", trainingId)); 34 | 35 | Training training = optionalTraining.get(); 36 | return learningRepo.exists(traineeId, training.courseId()); 37 | } 38 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/MailTemplate.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | 5 | import java.util.UUID; 6 | 7 | @Aggregate 8 | public class MailTemplate { 9 | private String id; 10 | private String template; 11 | private TemplateType templateType; 12 | 13 | public MailTemplate(String template, TemplateType templateType) { 14 | this.id = UUID.randomUUID().toString(); 15 | this.template = template; 16 | this.templateType = templateType; 17 | } 18 | 19 | public Notification compose(VariableContext context) { 20 | NotificationComposer notificationComposer = this.templateType.composer(template, context); 21 | return notificationComposer.compose(); 22 | } 23 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/MailTemplateException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class MailTemplateException extends DomainException { 6 | public MailTemplateException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/NominationNotificationComposer.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominator; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominee; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Ticket; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDate; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class NominationNotificationComposer extends NotificationComposer { 13 | private Training training; 14 | private Ticket ticket; 15 | private ValidDate validDate; 16 | private Nominator nominator; 17 | private Nominee nominee; 18 | 19 | public NominationNotificationComposer(String template, VariableContext context) { 20 | super(template, context); 21 | } 22 | 23 | @Override 24 | protected void setup(VariableContext context) { 25 | training = context.get("training"); 26 | ticket = context.get("ticket"); 27 | validDate = context.get("valid_date"); 28 | nominator = context.get("nominator"); 29 | nominee = context.get("nominee"); 30 | } 31 | 32 | @Override 33 | protected String renderFrom() { 34 | return "admin@eas.com"; 35 | } 36 | 37 | @Override 38 | protected String renderTo() { 39 | return nominee.email(); 40 | } 41 | 42 | @Override 43 | protected String renderSubject() { 44 | return "Ticket Nomination Notification"; 45 | } 46 | 47 | @Override 48 | protected List registerVariables() { 49 | List variables = new ArrayList<>(); 50 | variables.add(TemplateVariable.with("nomineeName", nominee.name())); 51 | variables.add(TemplateVariable.with("nominatorName", nominator.name())); 52 | variables.add(TemplateVariable.with("url", ticket.url())); 53 | variables.add(TemplateVariable.with("title", training.title())); 54 | variables.add(TemplateVariable.with("description", training.description())); 55 | variables.add(TemplateVariable.with("beginTime", training.beginTime().toString())); 56 | variables.add(TemplateVariable.with("endTime", training.endTime().toString())); 57 | variables.add(TemplateVariable.with("place", training.place())); 58 | variables.add(TemplateVariable.with("deadline", validDate.deadline().toString())); 59 | return variables; 60 | } 61 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/Notification.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | public class Notification { 4 | private String from; 5 | private String to; 6 | private String subject; 7 | private String body; 8 | 9 | public Notification(String from, String to, String subject, String body) { 10 | this.from = from; 11 | this.to = to; 12 | this.subject = subject; 13 | this.body = body; 14 | } 15 | 16 | public String from() { 17 | return from; 18 | } 19 | 20 | public String to() { 21 | return to; 22 | } 23 | 24 | public String subject() { 25 | return subject; 26 | } 27 | 28 | public String body() { 29 | return body; 30 | } 31 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/NotificationComposer.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import org.stringtemplate.v4.ST; 4 | 5 | import java.util.List; 6 | 7 | public abstract class NotificationComposer { 8 | private static final char BEGIN_VARIABLE = '$'; 9 | private static final char END_VARIABLE = '$'; 10 | protected String template; 11 | 12 | public NotificationComposer(String template, VariableContext context) { 13 | this.template = template; 14 | setup(context); 15 | } 16 | 17 | protected void setup(VariableContext context) {} 18 | 19 | public Notification compose() { 20 | String from = renderFrom(); 21 | String to = renderTo(); 22 | String subject = renderSubject(); 23 | String body = renderBody(); 24 | return new Notification(from, to, subject, body); 25 | } 26 | 27 | protected abstract String renderFrom(); 28 | 29 | protected abstract String renderSubject(); 30 | 31 | protected abstract String renderTo(); 32 | 33 | private String renderBody() { 34 | List variables = registerVariables(); 35 | ST st = new ST(template, BEGIN_VARIABLE, END_VARIABLE); 36 | for (TemplateVariable variable : variables) { 37 | st.add(variable.name(), variable.value()); 38 | } 39 | return st.render(); 40 | } 41 | 42 | protected abstract List registerVariables(); 43 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/NotificationService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import xyz.zhangyi.ddd.core.stereotype.DomainService; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.MailTemplateRepository; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominator; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominee; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Ticket; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 11 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingException; 12 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TrainingRepository; 13 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDate; 14 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDateException; 15 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.ValidDateRepository; 16 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDateType; 17 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.client.NotificationClient; 18 | 19 | import java.util.Optional; 20 | 21 | @Service 22 | @DomainService 23 | public class NotificationService { 24 | @Autowired 25 | private MailTemplateRepository templateRepository; 26 | @Autowired 27 | private NotificationClient notificationClient; 28 | @Autowired 29 | private TrainingRepository trainingRepository; 30 | @Autowired 31 | private ValidDateRepository validDateRepository; 32 | 33 | public void notifyNominee(Ticket ticket, Nominator nominator, Nominee nominee) { 34 | MailTemplate mailTemplate = retrieveMailTemplate(); 35 | Training training = retrieveTraining(ticket); 36 | ValidDate validDate = retrieveValidDate(ticket); 37 | 38 | VariableContext variableContext = buildVariableContext(ticket, nominator, nominee, training, validDate); 39 | Notification notification = mailTemplate.compose(variableContext); 40 | 41 | notificationClient.send(notification); 42 | } 43 | 44 | private MailTemplate retrieveMailTemplate() { 45 | Optional optionalMailTemplate = templateRepository.templateOf(TemplateType.Nomination); 46 | String mailTemplateNotFoundMessage = String.format("mail template by %s was not found.", TemplateType.Nomination); 47 | return optionalMailTemplate.orElseThrow(() -> new MailTemplateException(mailTemplateNotFoundMessage)); 48 | } 49 | 50 | private Training retrieveTraining(Ticket ticket) { 51 | Optional optionalTraining = trainingRepository.trainingOf(ticket.trainingId()); 52 | String trainingNotFoundMessage = String.format("training by id {%s} was not found.", ticket.trainingId()); 53 | return optionalTraining.orElseThrow(() -> new TrainingException(trainingNotFoundMessage)); 54 | } 55 | 56 | private ValidDate retrieveValidDate(Ticket ticket) { 57 | Optional optionalValidDate = validDateRepository.validDateOf(ticket.trainingId(), ValidDateType.PODeadline); 58 | String validDateNotFoundMessage = String.format("valid date by training id {%s} was not found.", ticket.trainingId()); 59 | return optionalValidDate.orElseThrow(() -> new ValidDateException(validDateNotFoundMessage)); 60 | } 61 | 62 | private VariableContext buildVariableContext(Ticket ticket, Nominator nominator, Nominee nominee, Training training, ValidDate validDate) { 63 | VariableContext variableContext = new VariableContext(); 64 | variableContext.put("ticket", ticket); 65 | variableContext.put("training", training); 66 | variableContext.put("valid_date", validDate); 67 | variableContext.put("nominator", nominator); 68 | variableContext.put("nominee", nominee); 69 | return variableContext; 70 | } 71 | 72 | public void setMailTemplateRepository(MailTemplateRepository templateRepository) { 73 | this.templateRepository = templateRepository; 74 | } 75 | 76 | public void setNotificationClient(NotificationClient notificationClient) { 77 | this.notificationClient = notificationClient; 78 | } 79 | 80 | public void setTrainingRepository(TrainingRepository trainingRepository) { 81 | this.trainingRepository = trainingRepository; 82 | } 83 | 84 | public void setValidDateRepository(ValidDateRepository validDateRepository) { 85 | this.validDateRepository = validDateRepository; 86 | } 87 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/TemplateType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | public enum TemplateType { 4 | Nomination { 5 | public NotificationComposer composer(String template, VariableContext context) { 6 | return new NominationNotificationComposer(template, context); 7 | } 8 | }; 9 | 10 | abstract NotificationComposer composer(String template, VariableContext context); 11 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/TemplateVariable.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | public class TemplateVariable { 4 | private final String name; 5 | private final String value; 6 | 7 | private TemplateVariable(String name, String value) { 8 | this.name = name; 9 | this.value = value; 10 | } 11 | 12 | public static TemplateVariable with(String name, String value) { 13 | return new TemplateVariable(name, value); 14 | } 15 | 16 | public String name() { 17 | return name; 18 | } 19 | 20 | public String value() { 21 | return value; 22 | } 23 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/VariableContext.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class VariableContext { 7 | private Map variables = new HashMap<>(); 8 | 9 | public void put(String variableName, Object dataHolder) { 10 | variables.putIfAbsent(variableName.toLowerCase(), dataHolder); 11 | } 12 | 13 | public T get(String variableName) { 14 | return (T) variables.get(variableName); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/NominationService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import xyz.zhangyi.ddd.core.stereotype.DomainService; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception.NominationException; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.learning.LearningService; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification.NotificationService; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 11 | 12 | @Service 13 | @DomainService 14 | public class NominationService { 15 | @Autowired 16 | private LearningService learningService; 17 | @Autowired 18 | private TicketService ticketService; 19 | @Autowired 20 | private NotificationService notificationService; 21 | 22 | public void setLearningService(LearningService learningService) { 23 | this.learningService = learningService; 24 | } 25 | 26 | public void setTicketService(TicketService ticketService) { 27 | this.ticketService = ticketService; 28 | } 29 | 30 | public void setNotificationService(NotificationService notificationService) { 31 | this.notificationService = notificationService; 32 | } 33 | 34 | public void nominate(TicketId ticketId, TrainingId trainingId, Candidate candidate, Nominator nominator) { 35 | boolean beLearned = learningService.beLearned(candidate.employeeId(), trainingId); 36 | if (beLearned) { 37 | throw new NominationException(String.format("can not nominate the candidate %s who had learned in the training", candidate.name())); 38 | } 39 | Ticket ticket = ticketService.nominate(ticketId, nominator, candidate); 40 | notificationService.notifyNominee(ticket, nominator, candidate.toNominee()); 41 | } 42 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/Nominator.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.Operator; 4 | 5 | import java.util.Objects; 6 | 7 | public class Nominator { 8 | private String employeeId; 9 | private String name; 10 | private String email; 11 | private TrainingRole role; 12 | 13 | public Nominator(String employeeId, String name, String email, TrainingRole role) { 14 | this.employeeId = employeeId; 15 | this.name = name; 16 | this.email = email; 17 | this.role = role; 18 | } 19 | 20 | public String employeeId() { 21 | return this.employeeId; 22 | } 23 | 24 | public String name() { 25 | return this.name; 26 | } 27 | 28 | public Operator toOperator() { 29 | return new Operator(employeeId(), name()); 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | Nominator nominator = (Nominator) o; 37 | return employeeId.equals(nominator.employeeId) && 38 | name.equals(nominator.name) && 39 | email.equals(nominator.email) && 40 | role == nominator.role; 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | return Objects.hash(employeeId, name, email, role); 46 | } 47 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/Nominee.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import java.util.Objects; 4 | 5 | public class Nominee { 6 | private String employeeId; 7 | private String name; 8 | private String email; 9 | 10 | public Nominee(String employeeId, String name, String email) { 11 | this.employeeId = employeeId; 12 | this.name = name; 13 | this.email = email; 14 | } 15 | 16 | public String employeeId() { 17 | return this.employeeId; 18 | } 19 | 20 | public String name() { 21 | return this.name; 22 | } 23 | 24 | public String email() { 25 | return email; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | Nominee nominee = (Nominee) o; 33 | return employeeId.equals(nominee.employeeId) && 34 | name.equals(nominee.name) && 35 | email.equals(nominee.email); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return Objects.hash(employeeId, name, email); 41 | } 42 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/Ticket.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception.TicketException; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.OperationType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.StateTransit; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.TicketHistory; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | @Aggregate 14 | public class Ticket { 15 | private TicketId id; 16 | private TrainingId trainingId; 17 | private TicketStatus ticketStatus; 18 | private String nomineeId; 19 | 20 | public Ticket(TicketId id, TrainingId trainingId) { 21 | this(id, trainingId, TicketStatus.Available, null); 22 | } 23 | 24 | public Ticket(TicketId id, TrainingId trainingId, TicketStatus ticketStatus) { 25 | this(id, trainingId, ticketStatus, null); 26 | } 27 | 28 | public Ticket(TicketId id, TrainingId trainingId, TicketStatus ticketStatus, String nomineeId) { 29 | this.id = id; 30 | this.trainingId = trainingId; 31 | this.ticketStatus = ticketStatus; 32 | this.nomineeId = nomineeId; 33 | } 34 | 35 | public TicketHistory nominate(Candidate candidate, Nominator nominator) { 36 | validateTicketStatus(); 37 | doNomination(candidate); 38 | return generateHistory(candidate, nominator); 39 | } 40 | 41 | private void validateTicketStatus() { 42 | if (!ticketStatus.isAvailable()) { 43 | throw new TicketException("ticket is not available, cannot be nominated."); 44 | } 45 | } 46 | 47 | private void doNomination(Candidate candidate) { 48 | this.ticketStatus = TicketStatus.WaitForConfirm; 49 | this.nomineeId = candidate.employeeId(); 50 | } 51 | 52 | private TicketHistory generateHistory(Candidate candidate, Nominator nominator) { 53 | return new TicketHistory(id, 54 | candidate.toOwner(), 55 | transitState(), 56 | OperationType.Nomination, 57 | nominator.toOperator(), 58 | LocalDateTime.now()); 59 | } 60 | 61 | private StateTransit transitState() { 62 | return StateTransit.from(TicketStatus.Available).to(this.ticketStatus); 63 | } 64 | 65 | public TicketStatus status() { 66 | return ticketStatus; 67 | } 68 | 69 | public String nomineeId() { 70 | return nomineeId; 71 | } 72 | 73 | public TicketId id() { 74 | return this.id; 75 | } 76 | 77 | public String url() { 78 | return String.format("http://www.eas.com/eas/tickets/%s", this.id().value()); 79 | } 80 | 81 | public TrainingId trainingId() { 82 | return trainingId; 83 | } 84 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/TicketId.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import java.util.Objects; 4 | import java.util.UUID; 5 | 6 | public class TicketId { 7 | private String value; 8 | 9 | private TicketId(String value) { 10 | this.value = value; 11 | } 12 | 13 | public static TicketId next() { 14 | return new TicketId(UUID.randomUUID().toString()); 15 | } 16 | 17 | public static TicketId from(String value) { 18 | return new TicketId(value); 19 | } 20 | 21 | public String value() { 22 | return value; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | TicketId ticketId = (TicketId) o; 30 | return value.equals(ticketId.value); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hash(value); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return value; 41 | } 42 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/TicketService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import xyz.zhangyi.ddd.core.stereotype.DomainService; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TicketRepository; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.CandidateRepository; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception.TicketException; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.TicketHistory; 11 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TicketHistoryRepository; 12 | 13 | import java.util.Optional; 14 | 15 | @Service 16 | @DomainService 17 | public class TicketService { 18 | @Autowired 19 | private TicketRepository tickRepo; 20 | @Autowired 21 | private TicketHistoryRepository ticketHistoryRepo; 22 | @Autowired 23 | private CandidateRepository candidateRepo; 24 | 25 | public Ticket nominate(TicketId ticketId, Nominator nominator, Candidate candidate) { 26 | Optional optionalTicket = tickRepo.ticketOf(ticketId, TicketStatus.Available); 27 | Ticket ticket = optionalTicket.orElseThrow(() -> availableTicketNotFound(ticketId)); 28 | 29 | TicketHistory ticketHistory = ticket.nominate(candidate, nominator); 30 | 31 | tickRepo.update(ticket); 32 | ticketHistoryRepo.add(ticketHistory); 33 | candidateRepo.remove(candidate); 34 | 35 | return ticket; 36 | } 37 | 38 | private TicketException availableTicketNotFound(TicketId ticketId) { 39 | return new TicketException(String.format("available ticket by id {%s} is not found.", ticketId)); 40 | } 41 | 42 | public void setTicketRepository(TicketRepository tickRepo) { 43 | this.tickRepo = tickRepo; 44 | } 45 | 46 | public void setTicketHistoryRepository(TicketHistoryRepository ticketHistoryRepository) { 47 | this.ticketHistoryRepo = ticketHistoryRepository; 48 | } 49 | 50 | public void setCandidateRepository(CandidateRepository candidateRepository) { 51 | this.candidateRepo = candidateRepository; 52 | } 53 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/TicketStatus.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | public enum TicketStatus { 4 | Available, WaitForConfirm, Confirm; 5 | 6 | public boolean isAvailable() { 7 | return this == Available; 8 | } 9 | 10 | public boolean isWaitForConfirm() { 11 | return this == WaitForConfirm; 12 | } 13 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/TrainingRole.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | public enum TrainingRole { 4 | Coordinator 5 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/OperationType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | public enum OperationType { 4 | Nomination 5 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/Operator.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | import java.util.Objects; 4 | 5 | public class Operator { 6 | private String operatorId; 7 | private String name; 8 | 9 | public Operator(String operatorId, String name) { 10 | this.operatorId = operatorId; 11 | this.name = name; 12 | } 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | if (this == o) return true; 17 | if (o == null || getClass() != o.getClass()) return false; 18 | Operator operator = (Operator) o; 19 | return operatorId.equals(operator.operatorId) && 20 | name.equals(operator.name); 21 | } 22 | 23 | @Override 24 | public int hashCode() { 25 | return Objects.hash(operatorId, name); 26 | } 27 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/StateTransit.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketStatus; 4 | 5 | import java.util.Objects; 6 | 7 | public class StateTransit { 8 | private TicketStatus from; 9 | private TicketStatus to; 10 | 11 | private StateTransit() { 12 | } 13 | 14 | public StateTransit(TicketStatus from, TicketStatus to) { 15 | this.from = from; 16 | this.to = to; 17 | } 18 | 19 | public static StateTransit from(TicketStatus from) { 20 | StateTransit stateTransit = new StateTransit(); 21 | stateTransit.from = from; 22 | return stateTransit; 23 | } 24 | 25 | public StateTransit to(TicketStatus to) { 26 | this.to = to; 27 | return this; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | StateTransit that = (StateTransit) o; 35 | return from == that.from && 36 | to == that.to; 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(from, to); 42 | } 43 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/TicketHistory.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketId; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketStatus; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.UUID; 9 | 10 | @Aggregate 11 | public class TicketHistory { 12 | private String id; 13 | private TicketId ticketId; 14 | private TicketOwner owner; 15 | private StateTransit stateTransit; 16 | private OperationType operationType; 17 | private Operator operatedBy; 18 | private LocalDateTime operatedAt; 19 | 20 | public TicketHistory(TicketId ticketId, 21 | TicketOwner owner, 22 | StateTransit stateTransit, 23 | OperationType operationType, 24 | Operator operatedBy, 25 | LocalDateTime operatedAt) { 26 | this(UUID.randomUUID().toString(), ticketId, owner, stateTransit, operationType, operatedBy, operatedAt); 27 | } 28 | 29 | public TicketHistory(String id, 30 | TicketId ticketId, 31 | TicketOwner owner, 32 | StateTransit stateTransit, 33 | OperationType operationType, 34 | Operator operatedBy, 35 | LocalDateTime operatedAt) { 36 | this.id = id; 37 | this.ticketId = ticketId; 38 | this.owner = owner; 39 | this.stateTransit = stateTransit; 40 | this.operationType = operationType; 41 | this.operatedBy = operatedBy; 42 | this.operatedAt = operatedAt; 43 | } 44 | 45 | public TicketHistory(String id, 46 | TicketId ticketId, 47 | String ownerId, 48 | TicketOwnerType ownerType, 49 | TicketStatus fromStatus, 50 | TicketStatus toStatus, 51 | OperationType operationType, 52 | String operatorId, 53 | String operatorName, 54 | LocalDateTime operatedAt) { 55 | this.id = id; 56 | this.ticketId = ticketId; 57 | this.owner = new TicketOwner(ownerId, ownerType); 58 | this.stateTransit = StateTransit.from(fromStatus).to(toStatus); 59 | this.operationType = operationType; 60 | this.operatedBy = new Operator(operatorId, operatorName); 61 | this.operatedAt = operatedAt; 62 | } 63 | 64 | public TicketId ticketId() { 65 | return this.ticketId; 66 | } 67 | 68 | public TicketOwner getTicketOwner() { 69 | return this.owner; 70 | } 71 | 72 | public StateTransit getStateTransit() { 73 | return this.stateTransit; 74 | } 75 | 76 | 77 | public OperationType operationType() { 78 | return this.operationType; 79 | } 80 | 81 | public Operator operatedBy() { 82 | return this.operatedBy; 83 | } 84 | 85 | public LocalDateTime operatedAt() { 86 | return this.operatedAt; 87 | } 88 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/TicketOwner.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | import java.util.Objects; 4 | 5 | public class TicketOwner { 6 | private String employeeId; 7 | private TicketOwnerType ownerType; 8 | 9 | public TicketOwner(String employeeId, TicketOwnerType ownerType) { 10 | this.employeeId = employeeId; 11 | this.ownerType = ownerType; 12 | } 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | if (this == o) return true; 17 | if (o == null || getClass() != o.getClass()) return false; 18 | TicketOwner that = (TicketOwner) o; 19 | return employeeId.equals(that.employeeId) && 20 | ownerType == that.ownerType; 21 | } 22 | 23 | @Override 24 | public int hashCode() { 25 | return Objects.hash(employeeId, ownerType); 26 | } 27 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/TicketOwnerType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | public enum TicketOwnerType { 4 | Nominee 5 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/training/Training.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Aggregate; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.UUID; 8 | 9 | @Aggregate 10 | public class Training { 11 | private TrainingId id; 12 | private String title; 13 | private String description; 14 | private LocalDateTime beginTime; 15 | private LocalDateTime endTime; 16 | private String place; 17 | private CourseId courseId; 18 | 19 | public Training(String title, String description, LocalDateTime beginTime, LocalDateTime endTime, String place, CourseId courseId) { 20 | this(TrainingId.from(UUID.randomUUID().toString()), title, description, beginTime, endTime, place, courseId); 21 | } 22 | 23 | public Training(TrainingId id, String title, String description, LocalDateTime beginTime, LocalDateTime endTime, String place, CourseId courseId) { 24 | this.id = id; 25 | this.title = title; 26 | this.description = description; 27 | this.beginTime = beginTime; 28 | this.endTime = endTime; 29 | this.place = place; 30 | this.courseId = courseId; 31 | } 32 | 33 | public TrainingId id() { 34 | return this.id; 35 | } 36 | 37 | public CourseId courseId() { 38 | return courseId; 39 | } 40 | 41 | public String title() { 42 | return this.title; 43 | } 44 | 45 | public String description() { 46 | return this.description; 47 | } 48 | 49 | public LocalDateTime beginTime() { 50 | return beginTime; 51 | } 52 | 53 | public LocalDateTime endTime() { 54 | return endTime; 55 | } 56 | 57 | public String place() { 58 | return place; 59 | } 60 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/training/TrainingException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class TrainingException extends DomainException { 6 | 7 | public TrainingException(String message) { 8 | super(message); 9 | } 10 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/training/TrainingId.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training; 2 | 3 | import xyz.zhangyi.ddd.core.domain.Identity; 4 | 5 | import java.util.Objects; 6 | import java.util.UUID; 7 | 8 | public class TrainingId implements Identity { 9 | private String value; 10 | 11 | public TrainingId(String value) { 12 | this.value = value; 13 | } 14 | 15 | public static TrainingId from(String value) { 16 | return new TrainingId(value); 17 | } 18 | 19 | public static TrainingId next() { 20 | return new TrainingId(UUID.randomUUID().toString()); 21 | } 22 | 23 | @Override 24 | public String value() { 25 | return this.value; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | TrainingId that = (TrainingId) o; 33 | return value.equals(that.value); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hash(value); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return value; 44 | } 45 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/training/TrainingService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import xyz.zhangyi.ddd.core.stereotype.DomainService; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TrainingRepository; 7 | 8 | import java.util.Optional; 9 | 10 | @Service 11 | @DomainService 12 | public class TrainingService { 13 | @Autowired 14 | private TrainingRepository trainingRepository; 15 | 16 | public Training trainingOf(TrainingId trainingId) { 17 | Optional optionalTraining = trainingRepository.trainingOf(trainingId); 18 | return optionalTraining.orElseThrow(() -> trainingNotFoundError(trainingId)); 19 | } 20 | 21 | private TrainingException trainingNotFoundError(TrainingId trainingId) { 22 | return new TrainingException(String.format("Training by id {%s} is not found.", trainingId)); 23 | } 24 | 25 | public void setTrainingRepository(TrainingRepository trainingRepo) { 26 | trainingRepository = trainingRepo; 27 | } 28 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/validate/ValidDate.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 4 | 5 | import java.time.LocalDateTime; 6 | import java.util.UUID; 7 | 8 | public class ValidDate { 9 | private String id; 10 | private TrainingId trainingId; 11 | private LocalDateTime deadline; 12 | private ValidDateType validDateType; 13 | 14 | public ValidDate(TrainingId trainingId, LocalDateTime deadline, ValidDateType validDateType) { 15 | this(UUID.randomUUID().toString(), trainingId, deadline, validDateType); 16 | } 17 | 18 | public ValidDate(String id, TrainingId trainingId, LocalDateTime deadline, ValidDateType validDateType) { 19 | this.id = id; 20 | this.trainingId = trainingId; 21 | this.deadline = deadline; 22 | this.validDateType = validDateType; 23 | } 24 | 25 | public LocalDateTime deadline() { 26 | return this.deadline; 27 | } 28 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/validate/ValidDateException.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate; 2 | 3 | import xyz.zhangyi.ddd.core.exception.DomainException; 4 | 5 | public class ValidDateException extends DomainException { 6 | public ValidDateException(String message) { 7 | super(message); 8 | } 9 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/validate/ValidDateType.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate; 2 | 3 | public enum ValidDateType { 4 | PODeadline 5 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/local/appservice/NominationAppService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.local.appservice; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.EnableTransactionManagement; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import xyz.zhangyi.ddd.core.exception.ApplicationDomainException; 8 | import xyz.zhangyi.ddd.core.exception.ApplicationException; 9 | import xyz.zhangyi.ddd.core.exception.ApplicationInfrastructureException; 10 | import xyz.zhangyi.ddd.core.exception.ApplicationValidationException; 11 | import xyz.zhangyi.ddd.core.exception.DomainException; 12 | import xyz.zhangyi.ddd.core.stereotype.Local; 13 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message.NominatingCandidateRequest; 14 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.NominationService; 15 | 16 | import java.util.Objects; 17 | 18 | @Service 19 | @EnableTransactionManagement 20 | @Local 21 | public class NominationAppService { 22 | @Autowired 23 | private NominationService nominationService; 24 | 25 | @Transactional(rollbackFor = ApplicationException.class) 26 | public void nominate(NominatingCandidateRequest nominatingCandidateRequest) { 27 | if (Objects.isNull(nominatingCandidateRequest)) { 28 | throw new ApplicationValidationException("nomination request can not be null"); 29 | } 30 | try { 31 | nominationService.nominate( 32 | nominatingCandidateRequest.getTicketId(), 33 | nominatingCandidateRequest.getTrainingId(), 34 | nominatingCandidateRequest.toCandidate(), 35 | nominatingCandidateRequest.toNominator()); 36 | } catch (DomainException ex) { 37 | throw new ApplicationDomainException(ex.getMessage(), ex); 38 | } catch (Exception ex) { 39 | throw new ApplicationInfrastructureException("Infrastructure Error", ex); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/local/appservice/TrainingAppService.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.local.appservice; 2 | 3 | import com.google.common.base.Strings; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | import xyz.zhangyi.ddd.core.exception.ApplicationDomainException; 7 | import xyz.zhangyi.ddd.core.exception.ApplicationInfrastructureException; 8 | import xyz.zhangyi.ddd.core.exception.ApplicationValidationException; 9 | import xyz.zhangyi.ddd.core.exception.DomainException; 10 | import xyz.zhangyi.ddd.core.stereotype.Local; 11 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message.TrainingResponse; 12 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 13 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 14 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingService; 15 | 16 | @Service 17 | @Local 18 | public class TrainingAppService { 19 | @Autowired 20 | private TrainingService trainingService; 21 | 22 | public TrainingResponse trainingOf(String trainingId) { 23 | if (Strings.isNullOrEmpty(trainingId)) { 24 | throw new ApplicationValidationException("TrainingId can not be null or empty"); 25 | } 26 | try { 27 | Training training = trainingService.trainingOf(TrainingId.from(trainingId)); 28 | // todo: fetch the course detail by training.courseId() by CourseService; 29 | return TrainingResponse.from(training); 30 | } catch (DomainException ex) { 31 | throw new ApplicationDomainException(ex.getMessage(), ex); 32 | } catch (Exception ex) { 33 | throw new ApplicationInfrastructureException(ex.getMessage(), ex); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/message/NominatingCandidateRequest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominator; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketId; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TrainingRole; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 8 | 9 | import java.io.Serializable; 10 | 11 | // Request Message: DTO 12 | public class NominatingCandidateRequest implements Serializable { 13 | private String ticketId; 14 | private String trainingId; 15 | private String candidateId; 16 | private String candidateName; 17 | private String candidateEmail; 18 | private String nominatorId; 19 | private String nominatorName; 20 | private String nominatorEmail; 21 | private TrainingRole nominatorRole; 22 | 23 | public NominatingCandidateRequest() { 24 | } 25 | 26 | public NominatingCandidateRequest(String ticketId, 27 | String trainingId, 28 | String candidateId, 29 | String candidateName, 30 | String candidateEmail, 31 | String nominatorId, 32 | String nominatorName, 33 | String nominatorEmail, 34 | TrainingRole nominatorRole) { 35 | this.ticketId = ticketId; 36 | this.trainingId = trainingId; 37 | this.candidateId = candidateId; 38 | this.candidateName = candidateName; 39 | this.candidateEmail = candidateEmail; 40 | this.nominatorId = nominatorId; 41 | this.nominatorName = nominatorName; 42 | this.nominatorEmail = nominatorEmail; 43 | this.nominatorRole = nominatorRole; 44 | } 45 | 46 | public TicketId getTicketId() { 47 | return TicketId.from(this.ticketId); 48 | } 49 | 50 | public TrainingId getTrainingId() { 51 | return TrainingId.from(this.trainingId); 52 | } 53 | 54 | public String getCandidateId() { 55 | return candidateId; 56 | } 57 | 58 | public String getCandidateName() { 59 | return candidateName; 60 | } 61 | 62 | public String getCandidateEmail() { 63 | return candidateEmail; 64 | } 65 | 66 | public String getNominatorId() { 67 | return nominatorId; 68 | } 69 | 70 | public String getNominatorName() { 71 | return nominatorName; 72 | } 73 | 74 | public String getNominatorEmail() { 75 | return nominatorEmail; 76 | } 77 | 78 | public TrainingRole getNominatorRole() { 79 | return nominatorRole; 80 | } 81 | 82 | public Candidate toCandidate() { 83 | return new Candidate(candidateId, candidateName, candidateEmail, TrainingId.from(trainingId)); 84 | } 85 | 86 | public Nominator toNominator() { 87 | return new Nominator(nominatorId, nominatorName, nominatorEmail, nominatorRole); 88 | } 89 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/message/TrainingResponse.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message; 2 | 3 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 4 | 5 | import java.io.Serializable; 6 | import java.time.LocalDateTime; 7 | 8 | public class TrainingResponse implements Serializable { 9 | private String trainingId; 10 | private String title; 11 | private String description; 12 | private LocalDateTime beginTime; 13 | private LocalDateTime endTime; 14 | private String place; 15 | 16 | public TrainingResponse( 17 | String trainingId, 18 | String title, 19 | String description, 20 | LocalDateTime beginTime, 21 | LocalDateTime endTime, 22 | String place) { 23 | this.trainingId = trainingId; 24 | this.title = title; 25 | this.description = description; 26 | this.beginTime = beginTime; 27 | this.endTime = endTime; 28 | this.place = place; 29 | } 30 | 31 | public static TrainingResponse from(Training training) { 32 | return new TrainingResponse( 33 | training.id().value(), 34 | training.title(), 35 | training.description(), 36 | training.beginTime(), 37 | training.endTime(), 38 | training.place()); 39 | } 40 | 41 | public String getTrainingId() { 42 | return trainingId; 43 | } 44 | 45 | public String getTitle() { 46 | return title; 47 | } 48 | 49 | public String getDescription() { 50 | return description; 51 | } 52 | 53 | public LocalDateTime getBeginTime() { 54 | return beginTime; 55 | } 56 | 57 | public LocalDateTime getEndTime() { 58 | return endTime; 59 | } 60 | 61 | public String getPlace() { 62 | return place; 63 | } 64 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/remote/resource/TicketResource.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.remote.resource; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.PutMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import xyz.zhangyi.ddd.core.gateway.north.Resources; 11 | import xyz.zhangyi.ddd.core.stereotype.Remote; 12 | import xyz.zhangyi.ddd.core.stereotype.RemoteType; 13 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.local.appservice.NominationAppService; 14 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message.NominatingCandidateRequest; 15 | 16 | import java.util.logging.Logger; 17 | 18 | @RestController 19 | @RequestMapping("/tickets") 20 | @Remote(RemoteType.Resource) 21 | public class TicketResource { 22 | private Logger logger = Logger.getLogger(TicketResource.class.getName()); 23 | 24 | @Autowired 25 | private NominationAppService nominationAppService; 26 | 27 | @PutMapping 28 | public ResponseEntity nominate(@RequestBody NominatingCandidateRequest nominatingCandidateRequest) { 29 | return Resources.with("nominate ticket") 30 | .onSuccess(HttpStatus.ACCEPTED) 31 | .onError(HttpStatus.BAD_REQUEST) 32 | .onFailed(HttpStatus.INTERNAL_SERVER_ERROR) 33 | .execute(() -> nominationAppService.nominate(nominatingCandidateRequest)); 34 | } 35 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/remote/resource/TrainingResource.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.remote.resource; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import xyz.zhangyi.ddd.core.gateway.north.Resources; 11 | import xyz.zhangyi.ddd.core.stereotype.Remote; 12 | import xyz.zhangyi.ddd.core.stereotype.RemoteType; 13 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.local.appservice.TrainingAppService; 14 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message.TrainingResponse; 15 | 16 | import java.util.logging.Logger; 17 | 18 | @RestController 19 | @RequestMapping("/trainings") 20 | @Remote(RemoteType.Resource) 21 | public class TrainingResource { 22 | private Logger logger = Logger.getLogger(TrainingResource.class.getName()); 23 | 24 | @Autowired 25 | private TrainingAppService trainingAppService; 26 | 27 | @GetMapping(value = "/{id}") 28 | public ResponseEntity findBy(@PathVariable String id) { 29 | return Resources.with("find training by id") 30 | .onSuccess(HttpStatus.OK) 31 | .onError(HttpStatus.BAD_REQUEST) 32 | .onFailed(HttpStatus.NOT_FOUND) 33 | .execute(() -> trainingAppService.trainingOf(id)); 34 | } 35 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/adapter/client/NotificationClientAdapter.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.adapter.client; 2 | 3 | import org.springframework.stereotype.Component; 4 | import xyz.zhangyi.ddd.core.stereotype.Adapter; 5 | import xyz.zhangyi.ddd.core.stereotype.PortType; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.client.NotificationClient; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification.Notification; 8 | 9 | @Adapter(PortType.Client) 10 | @Component 11 | public class NotificationClientAdapter implements NotificationClient { 12 | @Override 13 | public void send(Notification notification) { 14 | System.out.println("send the notification"); 15 | } 16 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/adapter/repository/typehandler/CourseIdTypeHandler.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.adapter.repository.typehandler; 2 | 3 | import org.apache.ibatis.type.BaseTypeHandler; 4 | import org.apache.ibatis.type.JdbcType; 5 | import org.apache.ibatis.type.MappedJdbcTypes; 6 | import org.apache.ibatis.type.MappedTypes; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 8 | 9 | import java.sql.CallableStatement; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | 14 | @MappedTypes(CourseId.class) 15 | @MappedJdbcTypes(JdbcType.VARCHAR) 16 | public class CourseIdTypeHandler extends BaseTypeHandler { 17 | @Override 18 | public void setNonNullParameter(PreparedStatement preparedStatement, int i, CourseId parameter, JdbcType jdbcType) throws SQLException { 19 | preparedStatement.setString(i, parameter.value()); 20 | } 21 | 22 | @Override 23 | public CourseId getNullableResult(ResultSet resultSet, String columnName) throws SQLException { 24 | return CourseId.from(resultSet.getString(columnName)); 25 | } 26 | 27 | @Override 28 | public CourseId getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException { 29 | return CourseId.from(resultSet.getString(columnIndex)); 30 | } 31 | 32 | @Override 33 | public CourseId getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException { 34 | return CourseId.from(callableStatement.getString(columnIndex)); 35 | } 36 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/adapter/repository/typehandler/TicketIdTypeHandler.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.adapter.repository.typehandler; 2 | 3 | import org.apache.ibatis.type.BaseTypeHandler; 4 | import org.apache.ibatis.type.JdbcType; 5 | import org.apache.ibatis.type.MappedJdbcTypes; 6 | import org.apache.ibatis.type.MappedTypes; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketId; 8 | 9 | import java.sql.CallableStatement; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | 14 | @MappedTypes(TicketId.class) 15 | @MappedJdbcTypes(JdbcType.VARCHAR) 16 | public class TicketIdTypeHandler extends BaseTypeHandler { 17 | @Override 18 | public void setNonNullParameter(PreparedStatement preparedStatement, int i, TicketId parameter, JdbcType jdbcType) throws SQLException { 19 | preparedStatement.setString(i, parameter.value()); 20 | } 21 | 22 | @Override 23 | public TicketId getNullableResult(ResultSet resultSet, String columnName) throws SQLException { 24 | return TicketId.from(resultSet.getString(columnName)); 25 | } 26 | 27 | @Override 28 | public TicketId getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException { 29 | return TicketId.from(resultSet.getString(columnIndex)); 30 | } 31 | 32 | @Override 33 | public TicketId getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException { 34 | return TicketId.from(callableStatement.getString(columnIndex)); 35 | } 36 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/adapter/repository/typehandler/TrainingIdTypeHandler.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.adapter.repository.typehandler; 2 | 3 | import org.apache.ibatis.type.BaseTypeHandler; 4 | import org.apache.ibatis.type.JdbcType; 5 | import org.apache.ibatis.type.MappedJdbcTypes; 6 | import org.apache.ibatis.type.MappedTypes; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 8 | 9 | import java.sql.CallableStatement; 10 | import java.sql.PreparedStatement; 11 | import java.sql.ResultSet; 12 | import java.sql.SQLException; 13 | 14 | @MappedTypes(TrainingId.class) 15 | @MappedJdbcTypes(JdbcType.VARCHAR) 16 | public class TrainingIdTypeHandler extends BaseTypeHandler { 17 | @Override 18 | public void setNonNullParameter(PreparedStatement preparedStatement, int i, TrainingId parameter, JdbcType jdbcType) throws SQLException { 19 | preparedStatement.setString(i, parameter.value()); 20 | } 21 | 22 | @Override 23 | public TrainingId getNullableResult(ResultSet resultSet, String columnName) throws SQLException { 24 | return TrainingId.from(resultSet.getString(columnName)); 25 | } 26 | 27 | @Override 28 | public TrainingId getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException { 29 | return TrainingId.from(resultSet.getString(columnIndex)); 30 | } 31 | 32 | @Override 33 | public TrainingId getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException { 34 | return TrainingId.from(callableStatement.getString(columnIndex)); 35 | } 36 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/client/NotificationClient.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.client; 2 | 3 | import xyz.zhangyi.ddd.core.stereotype.Port; 4 | import xyz.zhangyi.ddd.core.stereotype.PortType; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification.Notification; 6 | 7 | @Port(PortType.Client) 8 | public interface NotificationClient { 9 | void send(Notification notification); 10 | } 11 | -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/CandidateRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.springframework.stereotype.Repository; 5 | import xyz.zhangyi.ddd.core.stereotype.Port; 6 | import xyz.zhangyi.ddd.core.stereotype.PortType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 8 | 9 | @Mapper 10 | @Repository 11 | @Port(PortType.Repository) 12 | public interface CandidateRepository { 13 | void remove(Candidate candidate); 14 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/LearningRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.springframework.stereotype.Repository; 5 | import xyz.zhangyi.ddd.core.stereotype.Port; 6 | import xyz.zhangyi.ddd.core.stereotype.PortType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 8 | 9 | @Mapper 10 | @Repository 11 | @Port(PortType.Repository) 12 | public interface LearningRepository { 13 | boolean exists(String traineeId, CourseId courseId); 14 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/MailTemplateRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.springframework.stereotype.Repository; 5 | import xyz.zhangyi.ddd.core.stereotype.Port; 6 | import xyz.zhangyi.ddd.core.stereotype.PortType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification.MailTemplate; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification.TemplateType; 9 | 10 | import java.util.Optional; 11 | 12 | @Mapper 13 | @Repository 14 | @Port(PortType.Repository) 15 | public interface MailTemplateRepository { 16 | Optional templateOf(TemplateType templateType); 17 | } 18 | -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/TicketHistoryRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.springframework.stereotype.Repository; 5 | import java.util.Optional; 6 | 7 | import xyz.zhangyi.ddd.core.stereotype.Port; 8 | import xyz.zhangyi.ddd.core.stereotype.PortType; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketId; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.TicketHistory; 11 | 12 | @Mapper 13 | @Repository 14 | @Port(PortType.Repository) 15 | public interface TicketHistoryRepository { 16 | Optional latest(TicketId ticketId); 17 | void add(TicketHistory ticketHistory); 18 | void deleteBy(TicketId ticketId); 19 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/TicketRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import java.util.Optional; 4 | import org.apache.ibatis.annotations.Mapper; 5 | import org.springframework.stereotype.Repository; 6 | import xyz.zhangyi.ddd.core.stereotype.Port; 7 | import xyz.zhangyi.ddd.core.stereotype.PortType; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Ticket; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketId; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketStatus; 11 | 12 | @Mapper 13 | @Repository 14 | @Port(PortType.Repository) 15 | public interface TicketRepository { 16 | Optional ticketOf(TicketId ticketId, TicketStatus ticketStatus); 17 | void update(Ticket ticket); 18 | void add(Ticket ticket); 19 | void remove(Ticket ticket); 20 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/TrainingRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.springframework.stereotype.Repository; 5 | import xyz.zhangyi.ddd.core.stereotype.Port; 6 | import xyz.zhangyi.ddd.core.stereotype.PortType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 9 | 10 | import java.util.Optional; 11 | 12 | @Mapper 13 | @Repository 14 | @Port(PortType.Repository) 15 | public interface TrainingRepository { 16 | Optional trainingOf(TrainingId trainingId); 17 | void add(Training training); 18 | void remove(Training training); 19 | } -------------------------------------------------------------------------------- /eas-training/src/main/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/south/port/repository/ValidDateRepository.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository; 2 | 3 | import org.apache.ibatis.annotations.Mapper; 4 | import org.springframework.stereotype.Repository; 5 | import xyz.zhangyi.ddd.core.stereotype.Port; 6 | import xyz.zhangyi.ddd.core.stereotype.PortType; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDate; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDateType; 10 | 11 | import java.util.Optional; 12 | 13 | @Mapper 14 | @Repository 15 | @Port(PortType.Repository) 16 | public interface ValidDateRepository { 17 | Optional validDateOf(TrainingId trainingId, ValidDateType validDateType); 18 | void add(ValidDate validDate); 19 | void remove(ValidDate validDate); 20 | } 21 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/db/migration/V1__create_training_and_ticket_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE training ( 2 | id VARCHAR(50) NOT NULL, 3 | title VARCHAR(50) NOT NULL, 4 | description VARCHAR(200) NOT NULL, 5 | beginTime TIMESTAMP NOT NULL, 6 | endTime TIMESTAMP NOT NULL, 7 | place VARCHAR(200) NOT NULL, 8 | courseId VARCHAR(50) NOT NULL, 9 | PRIMARY KEY(id) 10 | ); 11 | 12 | CREATE TABLE ticket ( 13 | id VARCHAR(50) NOT NULL, 14 | ticketStatus VARCHAR(20) NOT NULL, 15 | trainingId VARCHAR(50) NOT NULL, 16 | nomineeId VARCHAR(50), 17 | PRIMARY KEY(id) 18 | ); 19 | 20 | CREATE TABLE ticket_history ( 21 | id VARCHAR(50) NOT NULL, 22 | ticketId VARCHAR(50) NOT NULL, 23 | ownerId VARCHAR(50) NOT NULL, 24 | ownerType VARCHAR(20) NOT NULL, 25 | fromStatus VARCHAR(20) NOT NULL, 26 | toStatus VARCHAR(20) NOT NULL, 27 | OperationType VARCHAR(20) NOT NULL, 28 | operatorId VARCHAR(50) NOT NULL, 29 | operatorName VARCHAR(20) NOT NULL, 30 | operatedAt TIMESTAMP NOT NULL, 31 | PRIMARY KEY(id) 32 | ); 33 | 34 | CREATE TABLE learning ( 35 | id VARCHAR(50) NOT NULL, 36 | courseId VARCHAR(50) NOT NULL, 37 | trainingId VARCHAR(50) NOT NULL, 38 | traineeId VARCHAR(50) NOT NULL, 39 | PRIMARY KEY(id) 40 | ); 41 | 42 | CREATE TABLE candidate ( 43 | id VARCHAR(50) NOT NULL, 44 | employeeId VARCHAR(50) NOT NULL, 45 | trainingId VARCHAR(50) NOT NULL, 46 | PRIMARY KEY(id) 47 | ); 48 | 49 | CREATE TABLE valid_date ( 50 | id VARCHAR(50) NOT NULL, 51 | trainingId VARCHAR(50) NOT NULL, 52 | deadline TIMESTAMP NOT NULL, 53 | validDateType VARCHAR(20) NOT NULL, 54 | PRIMARY KEY(id) 55 | ); 56 | 57 | CREATE TABLE mail_template ( 58 | id VARCHAR(50) NOT NULL, 59 | template VARCHAR(1000) NOT NULL, 60 | templateType VARCHAR(20) NOT NULL, 61 | PRIMARY KEY(id) 62 | ); -------------------------------------------------------------------------------- /eas-training/src/main/resources/db/migration/V2__insert_test_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO mail_template(id, template, templateType) 2 | values ("mailc3c9-f809-4a06-8fc4-ff7d330c86ca", "Hi $nomineeName$:\n$url$\n\nHere is the basic information of training:\nTitle: $title$\nDescription: $description$\nBegin time: $beginTime$\nEnd time: $endTime$\nPlace: $place$\n\nThanks! We're excited to have you in the training.\nEAS Team", "Nomination"); -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/CandidateMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | delete from candidate where employeeId =#{employeeId} and trainingId =#{trainingId} 12 | 13 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/LearningMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/MailTemplateMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/TicketHistoryMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | insert into ticket_history 38 | (id, ticketId, ownerId, ownerType, fromStatus, toStatus, operationType, operatorId, operatorName, operatedAt) 39 | values 40 | ( 41 | #{id}, 42 | #{ticketId}, #{ticketOwner.employeeId}, #{ticketOwner.ownerType}, 43 | #{stateTransit.from}, #{stateTransit.to}, #{operationType}, 44 | #{operatedBy.operatorId}, #{operatedBy.name}, #{operatedAt} 45 | ) 46 | 47 | 48 | 49 | delete from ticket_history where ticketId = #{ticketId} 50 | 51 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/TicketMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | update ticket 20 | 21 | trainingId=#{trainingId}, 22 | ticketStatus=#{ticketStatus}, 23 | nomineeId=#{nomineeId} 24 | 25 | 26 | 27 | 28 | insert into ticket 29 | (id, ticketStatus, trainingId, nomineeId) 30 | values 31 | ( 32 | #{id}, #{ticketStatus}, #{trainingId}, #{nomineeId} 33 | ) 34 | 35 | 36 | 37 | delete from ticket where id = #{id} 38 | 39 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/TrainingMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | insert into training 23 | (id, title, description, beginTime, endTime, place, courseId) 24 | values 25 | ( 26 | #{id}, #{title}, #{description}, 27 | #{beginTime}, #{endTime}, #{place}, 28 | #{courseId} 29 | ) 30 | 31 | 32 | 33 | delete from training where id = #{id} 34 | 35 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mapper/ValidDateMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 17 | 18 | 19 | insert into valid_date 20 | (id, trainingId, deadline, validDateType) 21 | values 22 | ( 23 | #{id}, #{trainingId}, #{deadline}, #{validDateType} 24 | ) 25 | 26 | 27 | 28 | delete from valid_date where trainingId = #{trainingId} and validDateType = #{validDateType} 29 | 30 | -------------------------------------------------------------------------------- /eas-training/src/main/resources/mybatis/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/learning/LearningServiceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.learning; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.LearningRepository; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingException; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TrainingRepository; 11 | 12 | import java.time.LocalDateTime; 13 | import java.util.Optional; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 17 | import static org.mockito.Mockito.*; 18 | 19 | public class LearningServiceTest { 20 | private TrainingId trainingId; 21 | private String candidateId; 22 | private CourseId courseId; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | trainingId = TrainingId.next(); 27 | candidateId = "200901010010"; 28 | courseId = CourseId.next(); 29 | } 30 | 31 | @Test 32 | public void should_return_true_if_candidate_is_in_learn_list() { 33 | LocalDateTime beginTime = LocalDateTime.of(2020, 1, 8, 9, 0); 34 | LocalDateTime endTime = LocalDateTime.of(2020, 1, 9, 17, 0); 35 | Training training = new Training(trainingId, "ddd", "ddd training", beginTime, endTime, "London Room", courseId); 36 | 37 | TrainingRepository mockTrainingRepo = mock(TrainingRepository.class); 38 | when(mockTrainingRepo.trainingOf(trainingId)).thenReturn(Optional.of(training)); 39 | 40 | LearningRepository mockLearningRepo = mock(LearningRepository.class); 41 | when(mockLearningRepo.exists(candidateId, training.courseId())).thenReturn(true); 42 | 43 | LearningService learningService = new LearningService(); 44 | learningService.setTrainingRepository(mockTrainingRepo); 45 | learningService.setLearningRepository(mockLearningRepo); 46 | 47 | boolean beLearned = learningService.beLearned(candidateId, trainingId); 48 | 49 | assertThat(beLearned).isTrue(); 50 | verify(mockTrainingRepo).trainingOf(trainingId); 51 | verify(mockLearningRepo).exists(candidateId, training.courseId()); 52 | } 53 | 54 | @Test 55 | public void should_throw_TrainingException_if_training_not_found() { 56 | TrainingRepository mockTrainingRepo = mock(TrainingRepository.class); 57 | when(mockTrainingRepo.trainingOf(trainingId)).thenReturn(Optional.empty()); 58 | 59 | LearningRepository mockLearningRepo = mock(LearningRepository.class); 60 | 61 | LearningService learningService = new LearningService(); 62 | learningService.setTrainingRepository(mockTrainingRepo); 63 | learningService.setLearningRepository(mockLearningRepo); 64 | 65 | assertThatThrownBy(() -> learningService.beLearned(candidateId, trainingId)) 66 | .isInstanceOf(TrainingException.class) 67 | .hasMessageContaining(String.format("training by id {%s} can not be found", trainingId)); 68 | 69 | verify(mockTrainingRepo).trainingOf(trainingId); 70 | } 71 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/notification/MailTemplateTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.Training; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDate; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.validate.ValidDateType; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.*; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | public class MailTemplateTest { 16 | @Test 17 | public void should_compose_nomination_notification_given_ticket_and_nominator_and_nominee() { 18 | // given 19 | Nominator nominator = createNominator(); 20 | Nominee nominee = createNominee(); 21 | TrainingId trainingId = TrainingId.next(); 22 | Training training = createTraining(trainingId); 23 | ValidDate validDate = createValidDate(trainingId); 24 | Ticket ticket = createTicket(); 25 | 26 | String template = buildTemplate(); 27 | VariableContext context = buildVariableContext(nominator, nominee, validDate, training, ticket); 28 | 29 | MailTemplate mailTemplate = new MailTemplate(template, TemplateType.Nomination); 30 | 31 | // when 32 | Notification notification = mailTemplate.compose(context); 33 | 34 | // then 35 | assertThat(notification.from()).isEqualTo("admin@eas.com"); 36 | assertThat(notification.to()).isEqualTo(nominee.email()); 37 | assertThat(notification.subject()).isEqualTo("Ticket Nomination Notification"); 38 | assertNotificationBody(nominator, nominee, validDate, training, ticket, notification); 39 | } 40 | 41 | private VariableContext buildVariableContext(Nominator nominator, Nominee nominee, ValidDate validDate, Training training, Ticket ticket) { 42 | VariableContext context = new VariableContext(); 43 | context.put("training", training); 44 | context.put("ticket", ticket); 45 | context.put("valid_date", validDate); 46 | context.put("nominator", nominator); 47 | context.put("nominee", nominee); 48 | return context; 49 | } 50 | 51 | private void assertNotificationBody(Nominator nominator, Nominee nominee, ValidDate validDate, Training training, Ticket ticket, Notification notification) { 52 | assertThat(notification.body()).containsIgnoringCase(String.format("Hi %s:", nominee.name())); 53 | assertThat(notification.body()).containsIgnoringCase(String.format("you are nominated by %s to attend the training", nominator.name())); 54 | assertThat(notification.body()).containsIgnoringCase(String.format("the deadline %s", validDate.deadline())); 55 | assertThat(notification.body()).containsIgnoringCase(ticket.url()); 56 | assertThat(notification.body()).containsIgnoringCase(String.format("Title: %s", training.title())); 57 | assertThat(notification.body()).containsIgnoringCase(String.format("Description: %s", training.description())); 58 | assertThat(notification.body()).containsIgnoringCase(String.format("Begin time: %s", training.beginTime())); 59 | assertThat(notification.body()).containsIgnoringCase(String.format("End time: %s", training.endTime())); 60 | assertThat(notification.body()).containsIgnoringCase(String.format("Place: %s", training.place())); 61 | } 62 | 63 | private Ticket createTicket() { 64 | return new Ticket(TicketId.next(), TrainingId.next()); 65 | } 66 | 67 | private String buildTemplate() { 68 | return "Hi $nomineeName$:\n" + 69 | "We are glad to notify that you are nominated by $nominatorName$ to attend the training. Please click the link as below to confirm this nomination before the deadline $deadline$:\n" + 70 | "$url$\n" + 71 | "\n" + 72 | "Here is the basic information of training:\n" + 73 | "Title: $title$\n" + 74 | "Description: $description$\n" + 75 | "Begin time: $beginTime$\n" + 76 | "End time: $endTime$\n" + 77 | "Place: $place$\n" + 78 | "\n" + 79 | "Thanks! We're excited to have you in the training.\n" + 80 | "EAS Team"; 81 | } 82 | 83 | private Training createTraining(TrainingId trainingId) { 84 | CourseId courseId = CourseId.next(); 85 | LocalDateTime beginTime = LocalDateTime.of(2020, 1, 8, 9, 0); 86 | LocalDateTime endTime = LocalDateTime.of(2020, 1, 9, 17, 0); 87 | String place = "London Room"; 88 | return new Training(trainingId, "ddd", "ddd training", beginTime, endTime, place, courseId); 89 | } 90 | 91 | private ValidDate createValidDate(TrainingId trainingId) { 92 | LocalDateTime poDeadline = LocalDateTime.of(2019, 12, 20, 0, 0); 93 | return new ValidDate(trainingId, poDeadline, ValidDateType.PODeadline); 94 | } 95 | 96 | private Nominee createNominee() { 97 | return new Nominee("201001010011", "zhangyi", "zhangyi@eas.com"); 98 | } 99 | 100 | private Nominator createNominator() { 101 | return new Nominator("200901010010", "bruce", "bruce@eas.com", TrainingRole.Coordinator); 102 | } 103 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/NominationServiceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception.NominationException; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.learning.LearningService; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.notification.NotificationService; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 9 | 10 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 11 | import static org.mockito.Mockito.*; 12 | 13 | public class NominationServiceTest { 14 | @Test 15 | public void should_nominate_candidate_to_be_nominee() { 16 | // given 17 | TicketId ticketId = TicketId.next(); 18 | TrainingId trainingId = TrainingId.next(); 19 | Ticket ticket = new Ticket(ticketId, trainingId, TicketStatus.WaitForConfirm); 20 | Candidate candidate = new Candidate(("200901010007"), "admin", "admin@eas.com", trainingId); 21 | Nominator nominator = new Nominator("200901010007", "admin", "admin@eas.com", TrainingRole.Coordinator); 22 | 23 | LearningService mockLearningService = mock(LearningService.class); 24 | when(mockLearningService.beLearned(candidate.employeeId(), trainingId)).thenReturn(false); 25 | 26 | TicketService mockTicketService = mock(TicketService.class); 27 | when(mockTicketService.nominate(ticketId, nominator, candidate)).thenReturn(ticket); 28 | 29 | NotificationService mockNotificationService = mock(NotificationService.class); 30 | 31 | NominationService nominationService = new NominationService(); 32 | nominationService.setLearningService(mockLearningService); 33 | nominationService.setTicketService(mockTicketService); 34 | nominationService.setNotificationService(mockNotificationService); 35 | 36 | // when 37 | nominationService.nominate(ticketId, trainingId, candidate, nominator); 38 | 39 | // then 40 | verify(mockLearningService).beLearned(candidate.employeeId(), trainingId); 41 | verify(mockTicketService).nominate(ticketId, nominator, candidate); 42 | verify(mockNotificationService).notifyNominee(ticket, nominator, candidate.toNominee()); 43 | } 44 | 45 | @Test 46 | public void should_throw_NominationException_if_candidate_had_been_learned() { 47 | // given 48 | TicketId ticketId = TicketId.next(); 49 | TrainingId trainingId = TrainingId.next(); 50 | Ticket ticket = new Ticket(ticketId, trainingId, TicketStatus.WaitForConfirm); 51 | Candidate candidate = new Candidate(("200901010007"), "admin", "admin@eas.com", trainingId); 52 | Nominator nominator = new Nominator("200901010007", "admin", "admin@eas.com", TrainingRole.Coordinator); 53 | 54 | LearningService mockLearningService = mock(LearningService.class); 55 | when(mockLearningService.beLearned(candidate.employeeId(), trainingId)).thenReturn(true); 56 | 57 | TicketService mockTicketService = mock(TicketService.class); 58 | when(mockTicketService.nominate(ticketId, nominator, candidate)).thenReturn(ticket); 59 | 60 | NotificationService mockNotificationService = mock(NotificationService.class); 61 | 62 | NominationService nominationService = new NominationService(); 63 | nominationService.setLearningService(mockLearningService); 64 | nominationService.setTicketService(mockTicketService); 65 | nominationService.setNotificationService(mockNotificationService); 66 | 67 | // then 68 | assertThatThrownBy(() -> nominationService.nominate(ticketId, trainingId, candidate, nominator)) 69 | .isInstanceOf(NominationException.class) 70 | .hasMessageContaining(String.format("can not nominate the candidate %s who had learned in the training", candidate.name())); 71 | 72 | // then 73 | verify(mockLearningService).beLearned(candidate.employeeId(), trainingId); 74 | verify(mockTicketService, never()).nominate(ticketId, nominator, candidate); 75 | verify(mockNotificationService, never()).notifyNominee(ticket, nominator, candidate.toNominee()); 76 | } 77 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/TicketServiceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TicketRepository; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.CandidateRepository; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception.TicketException; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.TicketHistory; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TicketHistoryRepository; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 11 | 12 | import java.util.Optional; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 16 | import static org.mockito.Mockito.*; 17 | 18 | public class TicketServiceTest { 19 | @Test 20 | public void should_nominate_candidate_for_specific_ticket() { 21 | // given 22 | TrainingId trainingId = TrainingId.next(); 23 | TicketId ticketId = TicketId.next(); 24 | Ticket ticket = new Ticket(TicketId.next(), trainingId, TicketStatus.Available); 25 | 26 | TicketRepository mockTickRepo = mock(TicketRepository.class); 27 | when(mockTickRepo.ticketOf(ticketId, TicketStatus.Available)).thenReturn(Optional.of(ticket)); 28 | 29 | TicketHistoryRepository mockTicketHistoryRepo = mock(TicketHistoryRepository.class); 30 | CandidateRepository mockCandidateRepo = mock(CandidateRepository.class); 31 | 32 | TicketService ticketService = new TicketService(); 33 | ticketService.setTicketRepository(mockTickRepo); 34 | ticketService.setTicketHistoryRepository(mockTicketHistoryRepo); 35 | ticketService.setCandidateRepository(mockCandidateRepo); 36 | 37 | Candidate candidate = new Candidate("200901010110", "Tom", "tom@eas.com", trainingId); 38 | Nominator nominator = new Nominator("200901010007", "admin", "admin@eas.com", TrainingRole.Coordinator); 39 | 40 | // when 41 | Ticket nominatedTicket = ticketService.nominate(ticketId, nominator, candidate); 42 | 43 | // then 44 | assertThat(nominatedTicket.status().isWaitForConfirm()).isTrue(); 45 | verify(mockTickRepo).ticketOf(ticketId, TicketStatus.Available); 46 | verify(mockTickRepo).update(ticket); 47 | verify(mockTicketHistoryRepo).add(isA(TicketHistory.class)); 48 | verify(mockCandidateRepo).remove(candidate); 49 | } 50 | 51 | @Test 52 | public void should_throw_TicketException_if_available_ticket_not_found() { 53 | TicketId ticketId = TicketId.next(); 54 | TicketRepository mockTickRepo = mock(TicketRepository.class); 55 | when(mockTickRepo.ticketOf(ticketId, TicketStatus.Available)).thenReturn(Optional.empty()); 56 | 57 | TicketService ticketService = new TicketService(); 58 | ticketService.setTicketRepository(mockTickRepo); 59 | 60 | Candidate candidate = new Candidate("200901010110", "Tom", "tom@eas.com", TrainingId.next()); 61 | Nominator nominator = new Nominator("200901010007", "admin", "admin@eas.com", TrainingRole.Coordinator); 62 | 63 | assertThatThrownBy(() -> ticketService.nominate(ticketId, nominator, candidate)) 64 | .isInstanceOf(TicketException.class) 65 | .hasMessageContaining(String.format("available ticket by id {%s} is not found", ticketId.value())); 66 | verify(mockTickRepo).ticketOf(ticketId, TicketStatus.Available); 67 | } 68 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/ticket/TicketTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.exception.TicketException; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory.*; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 15 | 16 | public class TicketTest { 17 | private TrainingId trainingId; 18 | private Candidate candidate; 19 | private Nominator nominator; 20 | 21 | @Before 22 | public void setUp() { 23 | trainingId = TrainingId.next(); 24 | candidate = new Candidate("200901010110", "Tom", "tom@eas.com", trainingId); 25 | nominator = new Nominator("200901010007", "admin", "admin@eas.com", TrainingRole.Coordinator); 26 | } 27 | 28 | @Test 29 | public void should_generate_ticket_history_after_ticket_was_nominated() { 30 | Ticket ticket = new Ticket(TicketId.next(), trainingId); 31 | TicketHistory ticketHistory = ticket.nominate(candidate, nominator); 32 | assertTicketHistory(ticket, ticketHistory); 33 | } 34 | 35 | @Test 36 | public void ticket_status_should_be_WAIT_FOR_CONFIRM_after_ticket_was_nominated() { 37 | Ticket ticket = new Ticket(TicketId.next(), trainingId); 38 | 39 | ticket.nominate(candidate, nominator); 40 | 41 | assertThat(ticket.status()).isEqualTo(TicketStatus.WaitForConfirm); 42 | assertThat(ticket.nomineeId()).isEqualTo(candidate.employeeId()); 43 | } 44 | 45 | @Test 46 | public void should_throw_TicketException_given_ticket_is_not_AVAILABLE() { 47 | Ticket ticket = new Ticket(TicketId.next(), trainingId, TicketStatus.WaitForConfirm); 48 | 49 | assertThatThrownBy(() -> ticket.nominate(candidate, nominator)) 50 | .isInstanceOf(TicketException.class) 51 | .hasMessageContaining("ticket is not available"); 52 | } 53 | 54 | private void assertTicketHistory(Ticket ticket, TicketHistory ticketHistory) { 55 | Assertions.assertThat(ticketHistory.ticketId()).isEqualTo(ticket.id()); 56 | assertThat(ticketHistory.operationType()).isEqualTo(OperationType.Nomination); 57 | assertThat(ticketHistory.getTicketOwner()).isEqualTo(new TicketOwner(candidate.employeeId(), TicketOwnerType.Nominee)); 58 | assertThat(ticketHistory.getStateTransit()).isEqualTo(StateTransit.from(TicketStatus.Available).to(TicketStatus.WaitForConfirm)); 59 | assertThat(ticketHistory.operatedBy()).isEqualTo(new Operator(nominator.employeeId(), nominator.name())); 60 | assertThat(ticketHistory.operatedAt()).isEqualToIgnoringSeconds(LocalDateTime.now()); 61 | } 62 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/tickethistory/TicketHistoryRepositoryIT.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.tickethistory; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.test.context.ContextConfiguration; 8 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 9 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TicketHistoryRepository; 10 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketId; 11 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TicketStatus; 12 | 13 | import java.time.LocalDateTime; 14 | import java.util.Optional; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | 18 | @RunWith(SpringJUnit4ClassRunner.class) 19 | @ContextConfiguration("/spring-mybatis.xml") 20 | public class TicketHistoryRepositoryIT { 21 | @Autowired 22 | private TicketHistoryRepository ticketHistoryRepository; 23 | private final TicketId ticketId = TicketId.from("18e38931-822e-4012-a16e-ac65dfc56f8a"); 24 | 25 | @Before 26 | public void setup() { 27 | ticketHistoryRepository.deleteBy(ticketId); 28 | 29 | StateTransit availableToWaitForConfirm = StateTransit.from(TicketStatus.Available).to(TicketStatus.WaitForConfirm); 30 | LocalDateTime oldTime = LocalDateTime.of(2020, 1, 1, 12, 0, 0); 31 | TicketHistory oldHistory = createTicketHistory(availableToWaitForConfirm, oldTime); 32 | ticketHistoryRepository.add(oldHistory); 33 | 34 | StateTransit toConfirm = StateTransit.from(TicketStatus.WaitForConfirm).to(TicketStatus.Confirm); 35 | LocalDateTime newTime = LocalDateTime.of(2020, 1, 1, 13, 0, 0); 36 | TicketHistory newHistory = createTicketHistory(toConfirm, newTime); 37 | ticketHistoryRepository.add(newHistory); 38 | } 39 | 40 | @Test 41 | public void should_return_latest_one() { 42 | Optional latest = ticketHistoryRepository.latest(ticketId); 43 | 44 | assertThat(latest.isPresent()).isTrue(); 45 | assertThat(latest.get().getStateTransit()).isEqualTo(StateTransit.from(TicketStatus.WaitForConfirm).to(TicketStatus.Confirm)); 46 | // LocalDateTime expectedDateTime = LocalDateTime.of(2020, 1, 1, 13, 0, 0); 47 | // assertThat(latest.get().operatedAt().plusHours(8)).isEqualTo(expectedDateTime); 48 | } 49 | 50 | private TicketHistory createTicketHistory(StateTransit stateTransit, LocalDateTime operatedAt) { 51 | String ownerId = "201101011100"; 52 | TicketOwner owner = new TicketOwner(ownerId, TicketOwnerType.Nominee); 53 | String operatorId = "200101010100"; 54 | Operator operator = new Operator(operatorId, "admin"); 55 | return new TicketHistory(ticketId, owner, stateTransit, OperationType.Nomination, operator, operatedAt); 56 | } 57 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/domain/training/TrainingServiceTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.south.port.repository.TrainingRepository; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.course.CourseId; 6 | 7 | import java.time.LocalDateTime; 8 | import java.util.Optional; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.when; 14 | 15 | public class TrainingServiceTest { 16 | 17 | private final TrainingId trainingId = TrainingId.next(); 18 | 19 | @Test 20 | public void should_get_training_by_training_id() { 21 | TrainingRepository mockTrainingRepo = mock(TrainingRepository.class); 22 | when(mockTrainingRepo.trainingOf(trainingId)).thenReturn(Optional.of(createTraining())); 23 | 24 | TrainingService trainingService = new TrainingService(); 25 | trainingService.setTrainingRepository(mockTrainingRepo); 26 | 27 | Training actualTraining = trainingService.trainingOf(trainingId); 28 | 29 | assertThat(actualTraining).isNotNull(); 30 | assertThat(actualTraining.title()).isEqualTo("DDD"); 31 | } 32 | 33 | @Test 34 | public void should_throw_TrainingException_if_not_found() { 35 | TrainingRepository mockTrainingRepo = mock(TrainingRepository.class); 36 | when(mockTrainingRepo.trainingOf(trainingId)).thenReturn(Optional.empty()); 37 | 38 | TrainingService trainingService = new TrainingService(); 39 | trainingService.setTrainingRepository(mockTrainingRepo); 40 | 41 | assertThatThrownBy(() -> trainingService.trainingOf(trainingId)) 42 | .isInstanceOf(TrainingException.class) 43 | .hasMessageContaining(String.format("Training by id {%s} is not found", trainingId)); 44 | } 45 | 46 | private Training createTraining() { 47 | LocalDateTime beginTime = LocalDateTime.of(2020, 5, 20, 9, 0, 0); 48 | LocalDateTime endTime = LocalDateTime.of(2020, 5, 21, 17, 0, 0); 49 | CourseId courseId = CourseId.next(); 50 | return new Training(trainingId, "DDD", "DDD Description", beginTime, endTime, "Beijing Room", courseId); 51 | } 52 | } -------------------------------------------------------------------------------- /eas-training/src/test/java/xyz/zhangyi/ddd/eas/valueadded/trainingcontext/north/message/NominatingCandidateRequestTest.java: -------------------------------------------------------------------------------- 1 | package xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message; 2 | 3 | import org.junit.Test; 4 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.candidate.Candidate; 5 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.Nominator; 6 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.ticket.TrainingRole; 7 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.domain.training.TrainingId; 8 | import xyz.zhangyi.ddd.eas.valueadded.trainingcontext.north.message.NominatingCandidateRequest; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class NominatingCandidateRequestTest { 13 | private String ticketId; 14 | private String trainingId; 15 | private String candidateId; 16 | private String candidateName; 17 | private String candidateEmail; 18 | private String nominatorId; 19 | private String nominatorName; 20 | private String nominatorEmail; 21 | private TrainingRole nominatorRole; 22 | private NominatingCandidateRequest nominatingCandidateRequest; 23 | 24 | @Test 25 | public void should_compose_Candidate() { 26 | nominatingCandidateRequest = createNominationRequest(); 27 | 28 | Candidate candidate = nominatingCandidateRequest.toCandidate(); 29 | 30 | assertThat(candidate.employeeId()).isEqualTo(candidateId); 31 | assertThat(candidate.name()).isEqualTo(candidateName); 32 | assertThat(candidate.email()).isEqualTo(candidateEmail); 33 | assertThat(candidate.trainingId()).isEqualTo(TrainingId.from(trainingId)); 34 | } 35 | 36 | @Test 37 | public void should_compose_Nominator() { 38 | nominatingCandidateRequest = createNominationRequest(); 39 | Nominator expectedNominator = new Nominator(nominatorId, nominatorName, nominatorEmail, TrainingRole.Coordinator); 40 | 41 | Nominator nominator = nominatingCandidateRequest.toNominator(); 42 | 43 | assertThat(nominator).isEqualTo(expectedNominator); 44 | } 45 | 46 | private NominatingCandidateRequest createNominationRequest() { 47 | ticketId = "3070c3c9-f809-4a06-8fc4-ff7d330c86ca"; 48 | trainingId = "tt70c3c9-f809-4a06-8fc4-ff7d330c86ca"; 49 | candidateId = "201905051000"; 50 | candidateName = "guo jin"; 51 | candidateEmail = "jin.guo@eas.com"; 52 | nominatorId = "201001010111"; 53 | nominatorName = "ma yu"; 54 | nominatorEmail = "yu.ma@eas.com"; 55 | nominatorRole = TrainingRole.Coordinator; 56 | 57 | return new NominatingCandidateRequest( 58 | ticketId, 59 | trainingId, 60 | candidateId, 61 | candidateName, 62 | candidateEmail, 63 | nominatorId, 64 | nominatorName, 65 | nominatorEmail, 66 | nominatorRole); 67 | } 68 | } -------------------------------------------------------------------------------- /eas-training/src/test/resources/spring-mybatis.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/agiledon/eas-ddd.svg?branch=master)](https://travis-ci.org/agiledon/eas-ddd) 2 | [![codecov](https://codecov.io/gh/agiledon/eas-ddd/branch/master/graph/badge.svg)](https://codecov.io/gh/agiledon/eas-ddd) 3 | 4 | ### 项目介绍 5 | 6 | EAS-DDD是我为GitChat课程《[领域驱动设计实战(战略篇)](https://gitbook.cn/gitchat/column/5b3235082ab5224deb750e02)》与《[领域驱动设计实战(战术篇)](https://gitbook.cn/gitchat/column/5cbed2f6f00736695f3a8699)》提供的实战项目案例。该案例取材自我参与过的真实项目,整个案例的代码完全按照领域驱动设计的过程进行建模、设计与编码。 7 | 8 | 通过访问本Repository对应的[Wiki](https://github.com/agiledon/eas-ddd/wiki),可以了解EAS项目的需求、建模过程与建模产出物;更为详细的分析设计内容,请订阅我在GitChat上发布的课程。每个领域场景对应的用户故事、拆分的任务,请访问本Repository的[Issue](https://github.com/agiledon/eas-ddd/issues)。 9 | 10 | ### 环境 11 | 12 | 本项目的开发基于Java语言进行开发,具体环境包括: 13 | 14 | ``` 15 | Java: Java 8+ 16 | Maven: 3 17 | Spring: 5.1.10+ 18 | Spring Boot:2.1.9 19 | MyBatis:3.5.3 20 | Druid:1.1.20 21 | MySQL: 8.0 Community 22 | ``` 23 | 24 | 我个人认为JPA ORM更加符合DDD的设计,在另外一个采用DDD开发的项目[Payroll-DDD](https://github.com/agiledon/payroll-ddd),我选择的持久化框架就是Spring Data JPA。本项目之所以采用MyBatis,是考虑到MyBatis在国内企业软件开发领域中更为常见。同时,我也希望通过本项目说明使用MyBatis作为持久化框架,同样可以做DDD。 25 | 26 | ### 数据库环境准备 27 | 28 | 项目默认的数据库用户名为sa,密码为123456,数据库主机为localhost,数据库为eas-db。在安装了MySQL 8.0后,若数据库服务器信息与默认信息不同,请修改如下文件。在使用flywaydb执行数据库脚本时,需要确保数据库配置正确,并已经创建了eas-db数据库。 29 | 30 | **flywaydb的数据库配置** 31 | 32 | 在`pom.xml`文件的``中配置了如下内容: 33 | 34 | ```xml 35 | 36 | org.flywaydb 37 | flyway-maven-plugin 38 | ${flyway.version} 39 | 40 | ${db.driver} 41 | ${db.url} 42 | ${db.username} 43 | ${db.password} 44 | 45 | 46 | ``` 47 | 48 | 在同一个pom文件的属性配置部分,配置了数据库的相关属性: 49 | 50 | ```xml 51 | 52 | com.mysql.jdbc.Driver 53 | jdbc:mysql://localhost:3306/eas-db?serverTimezone=UTC 54 | sa 55 | 123456 56 | 57 | ``` 58 | 59 | 一旦准备好flywaydb的环境,就可以运行命令执行DB的清理: 60 | 61 | ``` 62 | mvn package flyway:clean 63 | ``` 64 | 65 | 或执行命令执行DB的迁移: 66 | 67 | ``` 68 | mvn package flyway:migrate 69 | ``` 70 | 71 | 若要忽略运行单元测试,可以在Maven命令后面加上参数: 72 | 73 | ``` 74 | -DskipTests 75 | ``` 76 | 77 | **测试环境准备** 78 | 79 | 本项目采用测试驱动开发对领域层的类与方法进行了编码实现,因而领域层的相关方法基本皆为单元测试所覆盖。 80 | 81 | 应用服务则通过集成测试保障其正确性。若要运行集成测试,需通过flywaydb执行SQL脚本,以准备集成测试所必须的数据库环境。如果数据库配置与我的配置不同,除了要修改`pom.xml`文件中的flywaydb的配置之外,还需要各个限界上下文模块`test/resources/spring-mybatis.xml`文件的相关配置,如: 82 | 83 | ```xml 84 | 91 | 92 | 93 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ``` 102 | 103 | #### 运行测试 104 | 105 | 默认情况下,如果运行`mvn test`则只会运行单元测试。如果确保数据库已经准备好,且通过flywaydb确保了数据库的表结构与测试数据已经准备好,可以运行`mvn integration-test`。该命令会运行所有测试,包括单元测试和集成测试。 106 | 107 | **注意:**项目中所有的单元测试以`Test`为测试类后缀,所有集成测试以`IT`为测试类后缀。 108 | 109 | ### 运行Spring Boot服务 110 | 111 | 整个项目采用了单体架构,故而所有限界上下文的远程服务都通过一个统一的主程序入口,即eas-entry模块下的EasApplication。Spring Boot的应用配置在该模块的resources文件夹下,文件名为`application.yml`,内容为: 112 | 113 | ```yml 114 | mybatis: 115 | mapperLocations: classpath:mapper/*.xml 116 | type-aliases-package: xyz.zhangyi.ddd.eas.trainingcontext.domain 117 | type-handlers-package: xyz.zhangyi.ddd.eas.trainingcontext.gateway.acl.impl.persistence.typehandlers 118 | 119 | spring: 120 | datasource: 121 | url: jdbc:mysql://localhost:3306/eas-db?serverTimezone=UTC 122 | username: sa 123 | password: 123456 124 | driver-class-name: com.mysql.cj.jdbc.Driver 125 | # 使用druid数据源 126 | type: com.alibaba.druid.pool.DruidDataSource 127 | ``` 128 | 129 | 若要启动Spring Boot服务,需确保数据库的配置正确。在没有配置端口的情况下,默认端口号为8080。你也可以在`application.yml`中指定端口。 130 | 131 | 可以直接在IDE下运行`EasApplication`启动Spring Boot服务。 132 | 133 | 如你所见,当前项目采用了单体架构,但是可以非常容易迁移到微服务架构。若采用Spring Boot公开服务,需要在每个模块的`pom.xml`文件中,加入`spring-boot-starter`的依赖。可以参考`eas-entry`模块的依赖配置。 --------------------------------------------------------------------------------