Skip to content

Commit

Permalink
Feature/game comment (#223)
Browse files Browse the repository at this point in the history
* feature: game comment

* feature: game sort by rating
  • Loading branch information
kuoche1712003 authored Nov 16, 2024
1 parent 7ade030 commit 5d917c5
Show file tree
Hide file tree
Showing 25 changed files with 618 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tw.waterballsa.gaas.application.repositories

import tw.waterballsa.gaas.domain.GameComment
import tw.waterballsa.gaas.domain.GameRegistration
import tw.waterballsa.gaas.domain.User

interface GameCommentRepository {
fun commentGame(gameComment: GameComment)
fun updateGameComment(gameComment: GameComment)
fun findByGameIdAndUserId(gameId: GameRegistration.Id, userId: User.Id): GameComment?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tw.waterballsa.gaas.application.usecases

import tw.waterballsa.gaas.application.eventbus.EventBus
import tw.waterballsa.gaas.application.repositories.GameCommentRepository
import tw.waterballsa.gaas.application.repositories.UserRepository
import tw.waterballsa.gaas.domain.GameComment
import tw.waterballsa.gaas.domain.GameRegistration
import tw.waterballsa.gaas.domain.User
import tw.waterballsa.gaas.events.CommentGameEvent
import tw.waterballsa.gaas.exceptions.NotFoundException.Companion.notFound
import tw.waterballsa.gaas.exceptions.PlatformException
import tw.waterballsa.gaas.exceptions.enums.PlatformError
import javax.inject.Named

@Named
class CommentGameUseCase(
private val userRepository: UserRepository,
private val gameRatingRepository: GameCommentRepository,
private val eventBus: EventBus,
) {

fun execute(request: Request) {
val commentUser = getCommentUser(request.identityProviderId)
val gameId = GameRegistration.Id(request.gameId)
val userId = commentUser.id!!

validateCommentEligibility(commentUser, gameId)
createGameComment(gameId, userId, request.rating, request.comment)
eventBus.broadcast(CommentGameEvent(gameId, userId, request.rating.toLong(), 1))
}

private fun getCommentUser(identityProviderId: String): User {
return userRepository.findByIdentity(identityProviderId)
?: throw notFound(PlatformError.USER_NOT_FOUND, User::class).message()
}

private fun validateCommentEligibility(user: User, gameId: GameRegistration.Id) {
user.validateGamePlayed(gameId)
validateNoExistingRating(gameId, user.id!!)
}

private fun createGameComment(gameId: GameRegistration.Id, userId: User.Id, rating: Int, comment: String) {
val newRating = GameComment(gameId, userId, rating, comment)
gameRatingRepository.commentGame(newRating)
}

private fun User.validateGamePlayed(gameId: GameRegistration.Id) {
val playedGamesIds = playedGamesIds ?: emptySet()
if (gameId !in playedGamesIds) {
throw PlatformException(PlatformError.GAME_NOT_PLAYED, "Must play game before comment.")
}
}

private fun validateNoExistingRating(gameId: GameRegistration.Id, userId: User.Id) {
if (gameRatingRepository.findByGameIdAndUserId(gameId, userId) != null) {
throw PlatformException(PlatformError.GAME_COMMENT_DUPLICATED, "Game already commented.")
}
}

data class Request(
val identityProviderId: String,
val gameId: String,
val rating: Int,
val comment: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package tw.waterballsa.gaas.application.usecases

import tw.waterballsa.gaas.application.eventbus.EventBus
import tw.waterballsa.gaas.application.repositories.GameCommentRepository
import tw.waterballsa.gaas.application.repositories.UserRepository
import tw.waterballsa.gaas.domain.GameComment
import tw.waterballsa.gaas.domain.GameRegistration
import tw.waterballsa.gaas.domain.User
import tw.waterballsa.gaas.events.CommentGameEvent
import tw.waterballsa.gaas.exceptions.NotFoundException.Companion.notFound
import tw.waterballsa.gaas.exceptions.enums.PlatformError
import java.time.Instant
import javax.inject.Named

@Named
class UpdateGameCommentUseCase(
private val userRepository: UserRepository,
private val gameRatingRepository: GameCommentRepository,
private val eventBus: EventBus,
) {

fun execute(request: Request) {
val commentUser = getCommentUser(request.identityProviderId)
val gameId = GameRegistration.Id(request.gameId)
val userId = commentUser.id!!

val gameComment = gameRatingRepository.findByGameIdAndUserId(gameId, userId)
?: throw notFound(PlatformError.GAME_COMMENT_NOT_FOUND, GameComment::class).message()
val originRating = gameComment.rating

gameComment.apply {
rating = request.rating
comment = request.comment
lastUpdatedTime = Instant.now()
}

gameRatingRepository.updateGameComment(gameComment)
eventBus.broadcast(CommentGameEvent(gameId, userId, originRating - request.rating.toLong(), 0))
}

private fun getCommentUser(identityProviderId: String): User {
return userRepository.findByIdentity(identityProviderId)
?: throw notFound(PlatformError.USER_NOT_FOUND, User::class).message()
}

data class Request(
val identityProviderId: String,
val gameId: String,
val rating: Int,
val comment: String,
)
}
19 changes: 19 additions & 0 deletions domain/src/main/kotlin/tw/waterballsa/gaas/domain/GameComment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tw.waterballsa.gaas.domain

import java.time.Instant

class GameComment(
val id: Id? = null,
val gameId: GameRegistration.Id,
val userId: User.Id,
var rating: Int,
var comment: String,
var lastUpdatedTime: Instant,
val createdTime: Instant,
) {
constructor(gameId: GameRegistration.Id, userId: User.Id, rating: Int, comment: String) :
this(null, gameId, userId, rating, comment, Instant.now(), Instant.now())

@JvmInline
value class Id(val value: String)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package tw.waterballsa.gaas.domain

import java.math.BigDecimal
import java.math.RoundingMode
import java.time.Instant

class GameRegistration(
Expand All @@ -14,7 +16,21 @@ class GameRegistration(
var frontEndUrl: String,
var backEndUrl: String,
val createdOn: Instant,
val totalRating: Long? = null,
val numberOfComments: Long? = null,
) {
@JvmInline
value class Id(val value: String)

fun rating(): Double {
val total = totalRating ?: 0
val number = numberOfComments ?: 0
return if (number == 0L) {
0.0
} else {
BigDecimal.valueOf(total)
.divide(BigDecimal.valueOf(number), 1, RoundingMode.HALF_UP)
.toDouble()
}
}
}
4 changes: 2 additions & 2 deletions domain/src/main/kotlin/tw/waterballsa/gaas/domain/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class User(
val email: String = "",
var nickname: String = "",
val identities: MutableList<String> = mutableListOf(),
val lastPlayedGameId: String? = null,
val playedGamesIds: Set<String>? = null,
val lastPlayedGameId: GameRegistration.Id? = null,
val playedGamesIds: Set<GameRegistration.Id>? = null,
) {
@JvmInline
value class Id(val value: String)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tw.waterballsa.gaas.events

import tw.waterballsa.gaas.domain.GameRegistration
import tw.waterballsa.gaas.domain.User

data class CommentGameEvent(
val gameId: GameRegistration.Id,
val userId: User.Id,
val incrementRating: Long,
val incrementNumberOfComments: Long,
) : DomainEvent()
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ enum class PlatformError(
GAME_START_FAILED("G005"),
GAME_NOT_STARTED("G006"),

GAME_NOT_PLAYED("G007"),
GAME_COMMENT_DUPLICATED("G008"),
GAME_COMMENT_NOT_FOUND("G009"),

USER_NOT_FOUND("U001"),
USER_INPUT_INVALID("U002"),
USER_NAME_DUPLICATED("U003"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package tw.waterballsa.gaas.spring.controllers

import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*
import tw.waterballsa.gaas.application.usecases.CommentGameUseCase
import tw.waterballsa.gaas.application.usecases.UpdateGameCommentUseCase
import tw.waterballsa.gaas.exceptions.PlatformException
import tw.waterballsa.gaas.exceptions.enums.PlatformError.JWT_ERROR
import tw.waterballsa.gaas.spring.controllers.viewmodel.PlatformViewModel

@RestController
@RequestMapping("/comments")
class GameCommentController(
private val commentGameUserCase: CommentGameUseCase,
private val updateGameCommentUseCase: UpdateGameCommentUseCase,
) {

@PostMapping
fun commentGame(
@AuthenticationPrincipal jwt: Jwt,
@RequestBody request: CommentGameRequest
): PlatformViewModel {
commentGameUserCase.execute(
CommentGameUseCase.Request(
jwt.identityProviderId,
request.gameId,
request.rating,
request.comment
)
)
return PlatformViewModel.success()
}


@PostMapping("/games/{gameId}")
fun updateGameComment(
@AuthenticationPrincipal jwt: Jwt,
@PathVariable gameId: String,
@RequestBody request: UpdateGameCommentRequest
): PlatformViewModel {
updateGameCommentUseCase.execute(
UpdateGameCommentUseCase.Request(
jwt.identityProviderId,
gameId,
request.rating,
request.comment,
)
)
return PlatformViewModel.success()
}


data class CommentGameRequest(
val gameId: String,
val rating: Int,
val comment: String,
)

data class UpdateGameCommentRequest(
val rating: Int,
val comment: String,
)
}

private val Jwt.identityProviderId: String
get() = subject ?: throw PlatformException(JWT_ERROR, "identityProviderId should exist.")
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ class GetGameRegistrationPresenter : GetGameRegistrationsUsecase.Presenter {
minPlayers = minPlayers,
maxPlayers = maxPlayers,
createdOn = createdOn,
rating = rating(),
numberOfComments = numberOfComments ?: 0L
)

data class GetGamesViewModel(
Expand All @@ -142,5 +144,7 @@ class GetGameRegistrationPresenter : GetGameRegistrationsUsecase.Presenter {
val minPlayers: Int,
val maxPlayers: Int,
val createdOn: Instant,
val rating: Double,
val numberOfComments: Long,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class GetUserPresenter : GetUserUseCase.Presenter {
id = id!!.value,
email = email,
nickname = nickname,
lastPlayedGameId = lastPlayedGameId,
lastPlayedGameId = lastPlayedGameId?.value,
playedGamesIds = playedGamesIds?.map { it.value }?.toSet(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ data class GetUserViewModel(
val email: String,
val nickname: String,
val lastPlayedGameId: String?,
val playedGamesIds: Set<String>?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tw.waterballsa.gaas.spring.eventbus

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import tw.waterballsa.gaas.events.CommentGameEvent
import tw.waterballsa.gaas.spring.repositories.dao.GameRegistrationDAO
import kotlin.reflect.KClass

@Component
class CommentGameEventListener(
override val eventType: KClass<CommentGameEvent>,
private val gameRegistrationDAO: GameRegistrationDAO,
) : EventListener<CommentGameEvent> {

@Autowired
constructor(gameRegistrationDAO: GameRegistrationDAO): this(CommentGameEvent::class, gameRegistrationDAO)

override fun onEvents(events: List<CommentGameEvent>) {
events.forEach {
gameRegistrationDAO.incrementTotalRatingAndNumberOfCommentsById(
it.gameId.value, it.incrementRating, it.incrementNumberOfComments
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tw.waterballsa.gaas.spring.repositories

import org.springframework.stereotype.Component
import tw.waterballsa.gaas.application.repositories.GameCommentRepository
import tw.waterballsa.gaas.domain.GameComment
import tw.waterballsa.gaas.domain.GameRegistration
import tw.waterballsa.gaas.domain.User
import tw.waterballsa.gaas.spring.repositories.dao.GameCommentDAO
import tw.waterballsa.gaas.spring.repositories.data.toData

@Component
class SpringGameCommentRepository(
private val gameCommentDAO: GameCommentDAO,
): GameCommentRepository {
override fun commentGame(gameComment: GameComment) {
gameCommentDAO.save(gameComment.toData()).toDomain()
}

override fun updateGameComment(gameComment: GameComment) {
gameCommentDAO.save(gameComment.toData()).toDomain()
}

override fun findByGameIdAndUserId(gameId: GameRegistration.Id, userId: User.Id): GameComment? {
return gameCommentDAO.findByGameIdAndUserId(gameId.value, userId.value)?.toDomain()
}
}
Loading

0 comments on commit 5d917c5

Please sign in to comment.