Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package edu.ntnu.idi.idatt.backend.game.gameProgress.application

import edu.ntnu.idi.idatt.backend.shared.events.MapStopCompletedEvent
import edu.ntnu.idi.idatt.backend.shared.events.PupilGameCompletedEvent
import org.springframework.stereotype.Component
import org.springframework.transaction.event.TransactionPhase
import org.springframework.transaction.event.TransactionalEventListener

/**
* Keeps classroom-scoped aggregate game progress in sync with committed gameplay events.
*/
@Component
class PupilGameProgressEventListener(
private val pupilGameProgressService: PupilGameProgressService,
) {
/**
* Increments the persisted completed-stop counter after a stop completion commits.
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun onMapStopCompleted(event: MapStopCompletedEvent) {
pupilGameProgressService.incrementCompletedStopsCount(
pupilId = event.pupilUserId,
classroomId = event.classroomId,
)
}

/**
* Marks the final boss as completed after the game-completion event commits.
*/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
fun onGameCompleted(event: PupilGameCompletedEvent) {
pupilGameProgressService.markFinalBossCompleted(
pupilId = event.pupilUserId,
classroomId = event.classroomId,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import edu.ntnu.idi.idatt.backend.iam.application.IamUserService
import edu.ntnu.idi.idatt.backend.iam.security.CurrentUser
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.server.ResponseStatusException
import java.time.Instant
Expand Down Expand Up @@ -100,6 +101,38 @@ class PupilGameProgressService(
classroomId: Long,
): PupilGameProgress? = pupilGameProgressRepository.findByPupilIdAndClassroomId(pupilId, classroomId)

/**
* Increments the completed-stop counter for one pupil in one classroom.
*
* If no progress row exists for the tuple, the update is skipped.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun incrementCompletedStopsCount(
pupilId: Long,
classroomId: Long,
) {
val progress = pupilGameProgressRepository.findByPupilIdAndClassroomId(pupilId, classroomId) ?: return
progress.completedStopsCount += 1
progress.lastActivityAt = Instant.now()
pupilGameProgressRepository.save(progress)
}

/**
* Marks the classroom-scoped game as completed for one pupil.
*
* If no progress row exists for the tuple, the update is skipped.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun markFinalBossCompleted(
pupilId: Long,
classroomId: Long,
) {
val progress = pupilGameProgressRepository.findByPupilIdAndClassroomId(pupilId, classroomId) ?: return
progress.finalBossCompleted = true
progress.lastActivityAt = Instant.now()
pupilGameProgressRepository.save(progress)
}

/**
* Creates an empty classroom-scoped progress row for a pupil if it does not already exist.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package edu.ntnu.idi.idatt.backend.game.gameProgress

import edu.ntnu.idi.idatt.backend.game.gameProgress.application.PupilGameProgressEventListener
import edu.ntnu.idi.idatt.backend.game.gameProgress.application.PupilGameProgressService
import edu.ntnu.idi.idatt.backend.shared.events.MapStopCompletedEvent
import edu.ntnu.idi.idatt.backend.shared.events.PupilGameCompletedEvent
import org.junit.jupiter.api.Test
import org.mockito.Mockito

class PupilGameProgressEventListenerTests {
@Test
fun `completed stop delegates aggregate stop counter increment to service`() {
val service = Mockito.mock(PupilGameProgressService::class.java)
val listener = PupilGameProgressEventListener(service)

listener.onMapStopCompleted(MapStopCompletedEvent(pupilUserId = 1L, classroomId = 2L, stopId = 3L))

Mockito.verify(service).incrementCompletedStopsCount(1L, 2L)
}

@Test
fun `game completed delegates final boss completion to service`() {
val service = Mockito.mock(PupilGameProgressService::class.java)
val listener = PupilGameProgressEventListener(service)

listener.onGameCompleted(PupilGameCompletedEvent(pupilUserId = 1L, classroomId = 2L, stopId = 5L))

Mockito.verify(service).markFinalBossCompleted(1L, 2L)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package edu.ntnu.idi.idatt.backend.game.gameProgress

import edu.ntnu.idi.idatt.backend.classroom.application.ClassroomQueryService
import edu.ntnu.idi.idatt.backend.game.gameProgress.application.PupilGameProgressService
import edu.ntnu.idi.idatt.backend.game.gameProgress.domain.PupilGameProgress
import edu.ntnu.idi.idatt.backend.game.gameProgress.infrastructure.PupilGameProgressRepository
import edu.ntnu.idi.idatt.backend.iam.application.CurrentUserResolver
import edu.ntnu.idi.idatt.backend.iam.application.IamUserService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import java.time.Instant

class PupilGameProgressServiceTests {
@Test
fun `increment completed stops count updates aggregate`() {
val repository = Mockito.mock(PupilGameProgressRepository::class.java)
val service = service(repository)
val progress = PupilGameProgress(completedStopsCount = 2, lastActivityAt = Instant.EPOCH)

Mockito.`when`(repository.findByPupilIdAndClassroomId(1L, 2L)).thenReturn(progress)

service.incrementCompletedStopsCount(1L, 2L)

assertEquals(3, progress.completedStopsCount)
assertTrue(progress.lastActivityAt.isAfter(Instant.EPOCH))
Mockito.verify(repository).save(progress)
}

@Test
fun `mark final boss completed updates aggregate`() {
val repository = Mockito.mock(PupilGameProgressRepository::class.java)
val service = service(repository)
val progress = PupilGameProgress(finalBossCompleted = false, lastActivityAt = Instant.EPOCH)

Mockito.`when`(repository.findByPupilIdAndClassroomId(1L, 2L)).thenReturn(progress)

service.markFinalBossCompleted(1L, 2L)

assertTrue(progress.finalBossCompleted)
assertTrue(progress.lastActivityAt.isAfter(Instant.EPOCH))
Mockito.verify(repository).save(progress)
}

private fun service(repository: PupilGameProgressRepository): PupilGameProgressService =
PupilGameProgressService(
repository,
Mockito.mock(IamUserService::class.java),
Mockito.mock(ClassroomQueryService::class.java),
Mockito.mock(CurrentUserResolver::class.java),
)
}