├── .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 | [](https://travis-ci.org/agiledon/eas-ddd)
2 | [](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`模块的依赖配置。
--------------------------------------------------------------------------------