From b5e9535198646c244387d981239af1ba4dd581e7 Mon Sep 17 00:00:00 2001 From: eilifhl Date: Tue, 28 Apr 2026 16:23:57 +0200 Subject: [PATCH 1/3] add eventlistener to fix medal bug --- .../medals/application/MedalEventListener.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/kotlin/edu/ntnu/idi/idatt/backend/medals/application/MedalEventListener.kt 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..35055e2f --- /dev/null +++ b/src/main/kotlin/edu/ntnu/idi/idatt/backend/medals/application/MedalEventListener.kt @@ -0,0 +1,27 @@ +package edu.ntnu.idi.idatt.backend.medals.application + +import edu.ntnu.idi.idatt.backend.classroom.infrastructure.ClassroomRepository +import edu.ntnu.idi.idatt.backend.iam.infrastructure.UserRepository +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 + +@Component +class MedalEventListener( + private val medalService: MedalService, + private val userRepository: UserRepository, + private val classroomRepository: ClassroomRepository, +) { + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + fun onMapStopCompleted(event: MapStopCompletedEvent) { + val pupil = userRepository.findById(event.pupilUserId).orElse(null) ?: return + val classroom = classroomRepository.findById(event.classroomId).orElse(null) ?: return + + medalService.awardStopMedal( + pupil = pupil, + classroom = classroom, + stopId = event.stopId, + ) + } +} From 2714500a06cd670e6a0e139a0f5a3be8f68d97ec Mon Sep 17 00:00:00 2001 From: eilifhl Date: Tue, 28 Apr 2026 16:24:11 +0200 Subject: [PATCH 2/3] test --- .../backend/medals/MedalEventListenerTests.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt 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..f0b09741 --- /dev/null +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt @@ -0,0 +1,63 @@ +package edu.ntnu.idi.idatt.backend.medals + +import edu.ntnu.idi.idatt.backend.classroom.domain.Classroom +import edu.ntnu.idi.idatt.backend.classroom.infrastructure.ClassroomRepository +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.iam.infrastructure.UserRepository +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 +import java.util.Optional + +class MedalEventListenerTests { + @Test + fun `completed stop awards stop medal after commit`() { + val medalService = Mockito.mock(MedalService::class.java) + val userRepository = Mockito.mock(UserRepository::class.java) + val classroomRepository = Mockito.mock(ClassroomRepository::class.java) + val listener = MedalEventListener(medalService, userRepository, classroomRepository) + val pupil = pupil() + val classroom = classroom() + + Mockito.`when`(userRepository.findById(1L)).thenReturn(Optional.of(pupil)) + Mockito.`when`(classroomRepository.findById(2L)).thenReturn(Optional.of(classroom)) + + listener.onMapStopCompleted(MapStopCompletedEvent(pupilUserId = 1L, classroomId = 2L, stopId = 3L)) + + Mockito.verify(medalService).awardStopMedal(pupil, classroom, 3L) + } + + @Test + fun `missing pupil skips medal award`() { + val medalService = Mockito.mock(MedalService::class.java) + val userRepository = Mockito.mock(UserRepository::class.java) + val classroomRepository = Mockito.mock(ClassroomRepository::class.java) + val listener = MedalEventListener(medalService, userRepository, classroomRepository) + + Mockito.`when`(userRepository.findById(1L)).thenReturn(Optional.empty()) + + listener.onMapStopCompleted(MapStopCompletedEvent(pupilUserId = 1L, classroomId = 2L, stopId = 3L)) + + Mockito.verifyNoInteractions(medalService) + } + + 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", + ) +} From 71f46ce52ed857e42c60f622e6f13359e5de2d93 Mon Sep 17 00:00:00 2001 From: eilifhl Date: Wed, 29 Apr 2026 10:32:03 +0200 Subject: [PATCH 3/3] use classroomservice instead of repository --- .../medals/application/MedalEventListener.kt | 32 +++++++++++++++---- .../backend/medals/MedalEventListenerTests.kt | 32 ++++++++++--------- 2 files changed, 43 insertions(+), 21 deletions(-) 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 index 35055e2f..009c106b 100644 --- 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 @@ -1,22 +1,42 @@ package edu.ntnu.idi.idatt.backend.medals.application -import edu.ntnu.idi.idatt.backend.classroom.infrastructure.ClassroomRepository -import edu.ntnu.idi.idatt.backend.iam.infrastructure.UserRepository +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 userRepository: UserRepository, - private val classroomRepository: ClassroomRepository, + 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 pupil = userRepository.findById(event.pupilUserId).orElse(null) ?: return - val classroom = classroomRepository.findById(event.classroomId).orElse(null) ?: return + val authenticatedSubject = event.pupilUserId.toString() + val pupil = currentUserResolver.requireUser(authenticatedSubject) + val classroom = classroomService.getClassroomById(event.classroomId, authenticatedSubject) medalService.awardStopMedal( pupil = pupil, 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 index f0b09741..d9ebcb2a 100644 --- a/src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt +++ b/src/test/kotlin/edu/ntnu/idi/idatt/backend/medals/MedalEventListenerTests.kt @@ -1,30 +1,29 @@ 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.classroom.infrastructure.ClassroomRepository +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.iam.infrastructure.UserRepository 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 -import java.util.Optional class MedalEventListenerTests { @Test fun `completed stop awards stop medal after commit`() { val medalService = Mockito.mock(MedalService::class.java) - val userRepository = Mockito.mock(UserRepository::class.java) - val classroomRepository = Mockito.mock(ClassroomRepository::class.java) - val listener = MedalEventListener(medalService, userRepository, classroomRepository) + 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`(userRepository.findById(1L)).thenReturn(Optional.of(pupil)) - Mockito.`when`(classroomRepository.findById(2L)).thenReturn(Optional.of(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)) @@ -32,17 +31,20 @@ class MedalEventListenerTests { } @Test - fun `missing pupil skips medal award`() { + fun `listener resolves event ids through services`() { val medalService = Mockito.mock(MedalService::class.java) - val userRepository = Mockito.mock(UserRepository::class.java) - val classroomRepository = Mockito.mock(ClassroomRepository::class.java) - val listener = MedalEventListener(medalService, userRepository, classroomRepository) - - Mockito.`when`(userRepository.findById(1L)).thenReturn(Optional.empty()) + 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.verifyNoInteractions(medalService) + Mockito.verify(currentUserResolver).requireUser("1") + Mockito.verify(classroomService).getClassroomById(2L, "1") } private fun pupil(): User =