diff --git a/src/main/kotlin/edu/ntnu/idi/idatt/backend/medals/application/MedalEventListener.kt b/src/main/kotlin/edu/ntnu/idi/idatt/backend/medals/application/MedalEventListener.kt new file mode 100644 index 00000000..009c106b --- /dev/null +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/medals/application/MedalEventListener.kt @@ -0,0 +1,47 @@ +package edu.ntnu.idi.idatt.backend.medals.application + +import edu.ntnu.idi.idatt.backend.classroom.application.ClassroomService +import edu.ntnu.idi.idatt.backend.iam.application.CurrentUserResolver +import edu.ntnu.idi.idatt.backend.shared.events.MapStopCompletedEvent +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener + +/** + * Bridges committed domain events into medal-awarding side effects. + * + * Stop completion is published from the stop-progress flow as a domain event. This listener + * resolves the pupil and classroom through application services and delegates the actual + * award logic to [MedalService]. + * + * @property medalService application service that persists pupil medals + * @property currentUserResolver application service used to resolve the pupil from the event id + * @property classroomService application service used to resolve the classroom context + */ +@Component +class MedalEventListener( + private val medalService: MedalService, + private val currentUserResolver: CurrentUserResolver, + private val classroomService: ClassroomService, +) { + /** + * Awards the stop medal after the originating stop-completion transaction has committed. + * + * The event stores persisted ids, so the listener rehydrates the pupil and classroom before + * delegating to [MedalService.awardStopMedal]. + * + * @param event committed stop-completion event + */ + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun onMapStopCompleted(event: MapStopCompletedEvent) { + val authenticatedSubject = event.pupilUserId.toString() + val pupil = currentUserResolver.requireUser(authenticatedSubject) + val classroom = classroomService.getClassroomById(event.classroomId, authenticatedSubject) + + medalService.awardStopMedal( + pupil = pupil, + classroom = classroom, + stopId = event.stopId, + ) + } +} diff --git a/src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt b/src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt new file mode 100644 index 00000000..d9ebcb2a --- /dev/null +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt @@ -0,0 +1,65 @@ +package edu.ntnu.idi.idatt.backend.medals + +import edu.ntnu.idi.idatt.backend.classroom.application.ClassroomService +import edu.ntnu.idi.idatt.backend.classroom.domain.Classroom +import edu.ntnu.idi.idatt.backend.iam.application.CurrentUserResolver +import edu.ntnu.idi.idatt.backend.iam.domain.User +import edu.ntnu.idi.idatt.backend.iam.domain.UserRole +import edu.ntnu.idi.idatt.backend.iam.domain.UserStatus +import edu.ntnu.idi.idatt.backend.medals.application.MedalEventListener +import edu.ntnu.idi.idatt.backend.medals.application.MedalService +import edu.ntnu.idi.idatt.backend.shared.events.MapStopCompletedEvent +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class MedalEventListenerTests { + @Test + fun `completed stop awards stop medal after commit`() { + val medalService = Mockito.mock(MedalService::class.java) + val currentUserResolver = Mockito.mock(CurrentUserResolver::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) + val listener = MedalEventListener(medalService, currentUserResolver, classroomService) + val pupil = pupil() + val classroom = classroom() + + Mockito.`when`(currentUserResolver.requireUser("1")).thenReturn(pupil) + Mockito.`when`(classroomService.getClassroomById(2L, "1")).thenReturn(classroom) + + listener.onMapStopCompleted(MapStopCompletedEvent(pupilUserId = 1L, classroomId = 2L, stopId = 3L)) + + Mockito.verify(medalService).awardStopMedal(pupil, classroom, 3L) + } + + @Test + fun `listener resolves event ids through services`() { + val medalService = Mockito.mock(MedalService::class.java) + val currentUserResolver = Mockito.mock(CurrentUserResolver::class.java) + val classroomService = Mockito.mock(ClassroomService::class.java) + val listener = MedalEventListener(medalService, currentUserResolver, classroomService) + val pupil = pupil() + val classroom = classroom() + + Mockito.`when`(currentUserResolver.requireUser("1")).thenReturn(pupil) + Mockito.`when`(classroomService.getClassroomById(2L, "1")).thenReturn(classroom) + listener.onMapStopCompleted(MapStopCompletedEvent(pupilUserId = 1L, classroomId = 2L, stopId = 3L)) + + Mockito.verify(currentUserResolver).requireUser("1") + Mockito.verify(classroomService).getClassroomById(2L, "1") + } + + private fun pupil(): User = + User( + id = 1L, + username = "pupil-one", + passwordHash = "hash", + role = UserRole.PUPIL, + status = UserStatus.ACTIVE, + ) + + private fun classroom(): Classroom = + Classroom( + id = 2L, + ownerTeacherId = 9L, + title = "Test Classroom", + ) +}