diff --git a/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java b/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java index 5c8b0171..b9a49731 100644 --- a/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java +++ b/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import tv.codely.mooc.courses.application.create.CreateCourseCommand; +import tv.codely.mooc.courses.application.update.RenameCourseCommand; import tv.codely.shared.domain.DomainError; import tv.codely.shared.domain.bus.command.CommandBus; import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; @@ -34,6 +35,16 @@ public ResponseEntity index( return new ResponseEntity<>(HttpStatus.CREATED); } + @PutMapping(value = "/courses/{id}/renameCourse") + public ResponseEntity renameCourse( + @PathVariable String id, + @RequestBody Request request + ) throws CommandHandlerExecutionError { + dispatch(new RenameCourseCommand(id, request.name())); + + return new ResponseEntity<>(HttpStatus.OK); + } + @Override public HashMap, HttpStatus> errorMapping() { return null; diff --git a/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java b/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java index b912a3e6..91a75a3e 100644 --- a/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java +++ b/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java @@ -4,19 +4,18 @@ import tv.codely.mooc.courses.domain.CourseId; import tv.codely.mooc.courses.domain.CourseNotExist; import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.mooc.courses.domain.service.DomainCourseFinder; import tv.codely.shared.domain.Service; @Service public final class CourseFinder { - private final CourseRepository repository; + private final DomainCourseFinder domainCourseFinder; public CourseFinder(CourseRepository repository) { - this.repository = repository; + this.domainCourseFinder = new DomainCourseFinder(repository); } public CourseResponse find(CourseId id) throws CourseNotExist { - return repository.search(id) - .map(CourseResponse::fromAggregate) - .orElseThrow(() -> new CourseNotExist(id)); + return CourseResponse.fromAggregate(domainCourseFinder.find(id)); } } diff --git a/src/mooc/main/tv/codely/mooc/courses/application/update/CourseNameUpdater.java b/src/mooc/main/tv/codely/mooc/courses/application/update/CourseNameUpdater.java new file mode 100644 index 00000000..f64abcc4 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/courses/application/update/CourseNameUpdater.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses.application.update; + +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseName; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.mooc.courses.domain.service.DomainCourseFinder; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.EventBus; + +@Service +public class CourseNameUpdater { + private final CourseRepository repository; + private final DomainCourseFinder domainCourseFinder; + private final EventBus eventBus; + + public CourseNameUpdater(CourseRepository repository, EventBus eventBus) { + this.repository = repository; + this.eventBus = eventBus; + this.domainCourseFinder = new DomainCourseFinder(this.repository); + } + + public void renameCourse(final CourseId courseId, final CourseName newCourseName) { + final Course course = domainCourseFinder.find(courseId); + + this.repository.save(buildNewCourse(course, newCourseName)); + + this.eventBus.publish(course.pullDomainEvents()); + } + + private Course buildNewCourse(final Course course, final CourseName newCourseName) { + return Course.rename(course.id(), newCourseName, course.duration()); + } +} diff --git a/src/mooc/main/tv/codely/mooc/courses/application/update/RenameCourseCommand.java b/src/mooc/main/tv/codely/mooc/courses/application/update/RenameCourseCommand.java new file mode 100644 index 00000000..1a0a1225 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/courses/application/update/RenameCourseCommand.java @@ -0,0 +1,22 @@ +package tv.codely.mooc.courses.application.update; + +import tv.codely.shared.domain.bus.command.Command; + +public class RenameCourseCommand implements Command { + private final String id; + + public RenameCourseCommand(String id, String name) { + this.id = id; + this.name = name; + } + + private final String name; + + public String id() { + return id; + } + + public String name() { + return name; + } +} diff --git a/src/mooc/main/tv/codely/mooc/courses/application/update/RenameCourseCommandHandler.java b/src/mooc/main/tv/codely/mooc/courses/application/update/RenameCourseCommandHandler.java new file mode 100644 index 00000000..f77b2a30 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/courses/application/update/RenameCourseCommandHandler.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses.application.update; + +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseName; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class RenameCourseCommandHandler implements CommandHandler { + private final CourseNameUpdater courseNameUpdater; + + public RenameCourseCommandHandler(final CourseNameUpdater courseNameUpdater) { + this.courseNameUpdater = courseNameUpdater; + } + + @Override + public void handle(final RenameCourseCommand command) { + CourseId id = new CourseId(command.id()); + CourseName name = new CourseName(command.name()); + + courseNameUpdater.renameCourse(id, name); + } +} diff --git a/src/mooc/main/tv/codely/mooc/courses/domain/Course.java b/src/mooc/main/tv/codely/mooc/courses/domain/Course.java index ef44a894..ea771567 100644 --- a/src/mooc/main/tv/codely/mooc/courses/domain/Course.java +++ b/src/mooc/main/tv/codely/mooc/courses/domain/Course.java @@ -2,6 +2,7 @@ import tv.codely.shared.domain.AggregateRoot; import tv.codely.shared.domain.course.CourseCreatedDomainEvent; +import tv.codely.shared.domain.course.CourseRenamedDomainEvent; import java.util.Objects; @@ -30,6 +31,12 @@ public static Course create(CourseId id, CourseName name, CourseDuration duratio return course; } + public static Course rename(final CourseId id, final CourseName name, final CourseDuration duration) { + final Course course = new Course(id, name, duration); + course.record(new CourseRenamedDomainEvent(id.value(), name.value(), duration.value())); + return course; + } + public CourseId id() { return id; } diff --git a/src/mooc/main/tv/codely/mooc/courses/domain/service/DomainCourseFinder.java b/src/mooc/main/tv/codely/mooc/courses/domain/service/DomainCourseFinder.java new file mode 100644 index 00000000..dc7469e6 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/courses/domain/service/DomainCourseFinder.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.courses.domain.service; + +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.mooc.courses.domain.CourseRepository; + +public class DomainCourseFinder { + private final CourseRepository courseRepository; + + + public DomainCourseFinder(CourseRepository courseRepository) { + this.courseRepository = courseRepository; + } + + public Course find(CourseId id) throws CourseNotExist { + return courseRepository.search(id) + .orElseThrow(() -> new CourseNotExist(id)); + } +} diff --git a/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java b/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java index eb7454de..b3547bc8 100644 --- a/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java +++ b/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java @@ -20,4 +20,8 @@ protected void setUp() { public void shouldHaveSaved(Course course) { verify(repository, atLeastOnce()).save(course); } + + public void shouldNotHaveSaved() { + verify(repository, never()).save(any(Course.class)); + } } diff --git a/src/mooc/test/tv/codely/mooc/courses/application/update/RenameCourseCommandHandlerTest.java b/src/mooc/test/tv/codely/mooc/courses/application/update/RenameCourseCommandHandlerTest.java new file mode 100644 index 00000000..8237109f --- /dev/null +++ b/src/mooc/test/tv/codely/mooc/courses/application/update/RenameCourseCommandHandlerTest.java @@ -0,0 +1,62 @@ +package tv.codely.mooc.courses.application.update; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleUnitTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseMother; +import tv.codely.mooc.courses.domain.CourseName; +import tv.codely.mooc.courses.domain.CourseNotExist; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + + +class RenameCourseCommandHandlerTest extends CoursesModuleUnitTestCase { + + private static final String NEW_NAME = "new name"; + private RenameCourseCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new RenameCourseCommandHandler(new CourseNameUpdater(repository, eventBus)); + } + + @Test + @DisplayName("Should rename course correctly when exist the course") + void should_rename_course_correctly_when_exist_the_course() { + // Arrange + final Course courseMock = CourseMother.random(); + final RenameCourseCommand renameCourseCommand = new RenameCourseCommand(courseMock.id().value(), NEW_NAME); + final Course courseExpected = Course.rename(courseMock.id(), new CourseName(NEW_NAME), courseMock.duration()); + + when(super.repository.search(courseMock.id())).thenReturn(Optional.of(courseMock)); + + // Action + + handler.handle(renameCourseCommand); + + // Assert + shouldHaveSaved(courseExpected); + } + + @Test + @DisplayName("Should return error when rename course but it not exist") + void should_return_error_when_rename_course_but_it_not_exist() { + // Arrange + final Course courseMock = CourseMother.random(); + final RenameCourseCommand renameCourseCommand = new RenameCourseCommand(courseMock.id().value(), NEW_NAME); + + // Action + assertThatThrownBy(() -> handler.handle(renameCourseCommand)).isInstanceOf(CourseNotExist.class); + + // Assert + shouldNotHaveSaved(); + } + +} diff --git a/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java new file mode 100644 index 00000000..c90fb8e1 --- /dev/null +++ b/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java @@ -0,0 +1,94 @@ +package tv.codely.shared.domain.course; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class CourseRenamedDomainEvent extends DomainEvent { + private final String name; + private final String duration; + + public CourseRenamedDomainEvent() { + super(null); + + this.name = null; + this.duration = null; + } + + public CourseRenamedDomainEvent(String aggregateId, String name, String duration) { + super(aggregateId); + + this.name = name; + this.duration = duration; + } + + public CourseRenamedDomainEvent( + String aggregateId, + String eventId, + String occurredOn, + String name, + String duration + ) { + super(aggregateId, eventId, occurredOn); + + this.name = name; + this.duration = duration; + } + + @Override + public String eventName() { + return "course.renamed"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap() {{ + put("name", name); + put("duration", duration); + }}; + } + + @Override + public CourseRenamedDomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new CourseRenamedDomainEvent( + aggregateId, + eventId, + occurredOn, + (String) body.get("name"), + (String) body.get("duration") + ); + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CourseRenamedDomainEvent that = (CourseRenamedDomainEvent) o; + return name.equals(that.name) && + duration.equals(that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(name, duration); + } +}