core: restore compilation and tests (ports, DTOs, repo fixes, metrics)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
||||
import com.project.movienight.application.ports.input.GetFilmRatingsUseCase
|
||||
import com.project.movienight.application.ports.input.RateFilmCommand
|
||||
import com.project.movienight.application.ports.input.RateFilmUseCase
|
||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
import com.project.movienight.application.ports.output.IdGenerator
|
||||
import com.project.movienight.domain.exception.DomainException
|
||||
import com.project.movienight.domain.exception.EntityNotFoundException
|
||||
import com.project.movienight.domain.model.FilmRating
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class FilmRatingService(
|
||||
private val filmRepository: FilmRepositoryPort,
|
||||
private val filmRatingRepository: FilmRatingRepositoryPort,
|
||||
private val idGenerator: IdGenerator,
|
||||
private val businessMetricsService: BusinessMetricsService,
|
||||
) : RateFilmUseCase,
|
||||
GetFilmRatingsUseCase {
|
||||
override fun rate(command: RateFilmCommand): FilmRating {
|
||||
if (command.score !in 1..10) {
|
||||
throw DomainException("Film rating score must be between 1 and 10")
|
||||
}
|
||||
|
||||
filmRepository.findById(command.filmId)
|
||||
?: throw EntityNotFoundException(entity = "Film", id = command.filmId.toString())
|
||||
|
||||
val existingRating = filmRatingRepository.findByUserIdAndFilmId(command.userId, command.filmId)
|
||||
val now = LocalDateTime.now()
|
||||
|
||||
val rating =
|
||||
if (existingRating == null) {
|
||||
FilmRating(
|
||||
id = idGenerator.generateId(),
|
||||
userId = command.userId,
|
||||
filmId = command.filmId,
|
||||
score = command.score,
|
||||
note = command.note,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
)
|
||||
} else {
|
||||
existingRating.copy(score = command.score, note = command.note, updatedAt = now)
|
||||
}
|
||||
|
||||
val savedRating = filmRatingRepository.save(rating)
|
||||
businessMetricsService.recordRatingSubmitted()
|
||||
return savedRating
|
||||
}
|
||||
|
||||
override fun getRatings(userId: UUID): List<FilmRating> = filmRatingRepository.findByUserId(userId)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class FilmService(
|
||||
|
||||
try {
|
||||
log.debug(
|
||||
"Create film request received: title='{}', descriptionLength={}",
|
||||
"Create film request received: title='{}', descriptionLength={}'",
|
||||
command.title,
|
||||
command.description.length,
|
||||
)
|
||||
@@ -56,23 +56,29 @@ class FilmService(
|
||||
throw BlockedValueException(target = "Film", field = "description")
|
||||
}
|
||||
|
||||
val film =
|
||||
Film(
|
||||
id = idGenerator.generateId(),
|
||||
title = command.title,
|
||||
description = command.description,
|
||||
contentType = command.contentType,
|
||||
releaseYear = command.releaseYear,
|
||||
genres = command.genres,
|
||||
cast = command.cast,
|
||||
directors = command.directors,
|
||||
imdbRating = command.imdbRating,
|
||||
platformRating = command.platformRating,
|
||||
externalUrl = command.externalUrl,
|
||||
jellyfinItemId = command.jellyfinItemId,
|
||||
jellyfinLibraryId = command.jellyfinLibraryId,
|
||||
)
|
||||
return filmRepository.save(film)
|
||||
val film =
|
||||
Film(
|
||||
id = idGenerator.generateId(),
|
||||
title = command.title,
|
||||
description = command.description,
|
||||
contentType = command.contentType,
|
||||
releaseYear = command.releaseYear,
|
||||
genres = command.genres,
|
||||
cast = command.cast,
|
||||
directors = command.directors,
|
||||
imdbRating = command.imdbRating,
|
||||
platformRating = command.platformRating,
|
||||
externalUrl = command.externalUrl,
|
||||
jellyfinItemId = command.jellyfinItemId,
|
||||
jellyfinLibraryId = command.jellyfinLibraryId,
|
||||
)
|
||||
|
||||
val saved = filmRepository.save(film)
|
||||
filmCreatedCounter.increment()
|
||||
return saved
|
||||
} finally {
|
||||
sample.stop(createFilmTimer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun edit(
|
||||
@@ -95,30 +101,35 @@ class FilmService(
|
||||
throw BlockedValueException(target = "Film", field = "description")
|
||||
}
|
||||
|
||||
val film = filmRepository.findById(id)
|
||||
var film = filmRepository.findById(id)
|
||||
|
||||
if (film == null) {
|
||||
log.debug("Film not found for edit: id='{}'", id)
|
||||
throw EntityNotFoundException(entity = "Film", id = id.toString())
|
||||
}
|
||||
|
||||
film =
|
||||
film.copy(
|
||||
title = command.title,
|
||||
description = command.description,
|
||||
contentType = command.contentType,
|
||||
releaseYear = command.releaseYear,
|
||||
genres = command.genres,
|
||||
cast = command.cast,
|
||||
directors = command.directors,
|
||||
imdbRating = command.imdbRating,
|
||||
platformRating = command.platformRating,
|
||||
externalUrl = command.externalUrl,
|
||||
jellyfinItemId = command.jellyfinItemId,
|
||||
jellyfinLibraryId = command.jellyfinLibraryId,
|
||||
)
|
||||
film =
|
||||
film.copy(
|
||||
title = command.title,
|
||||
description = command.description,
|
||||
contentType = command.contentType,
|
||||
releaseYear = command.releaseYear,
|
||||
genres = command.genres,
|
||||
cast = command.cast,
|
||||
directors = command.directors,
|
||||
imdbRating = command.imdbRating,
|
||||
platformRating = command.platformRating,
|
||||
externalUrl = command.externalUrl,
|
||||
jellyfinItemId = command.jellyfinItemId,
|
||||
jellyfinLibraryId = command.jellyfinLibraryId,
|
||||
)
|
||||
|
||||
return filmRepository.save(film)
|
||||
val saved = filmRepository.save(film)
|
||||
filmEditedCounter.increment()
|
||||
return saved
|
||||
} finally {
|
||||
sample.stop(editFilmTimer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(id: UUID) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
||||
import com.project.movienight.application.ports.input.GetRecommendationsUseCase
|
||||
import com.project.movienight.application.ports.input.RecommendationQuery
|
||||
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
|
||||
import com.project.movienight.domain.model.ContentType
|
||||
import com.project.movienight.domain.model.Film
|
||||
import com.project.movienight.domain.model.RecommendationResult
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class RecommendationService(
|
||||
private val filmRepository: FilmRepositoryPort,
|
||||
private val filmLibraryRepository: FilmLibraryRepositoryPort,
|
||||
private val filmRatingRepository: FilmRatingRepositoryPort,
|
||||
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
||||
private val businessMetricsService: BusinessMetricsService,
|
||||
) : GetRecommendationsUseCase {
|
||||
override fun recommend(query: RecommendationQuery): List<RecommendationResult> {
|
||||
businessMetricsService.recordRecommendationRequest()
|
||||
val preferences = userPreferencesRepository.findByUserId(query.userId)
|
||||
val ratings = filmRatingRepository.findByUserId(query.userId).associateBy { it.filmId }
|
||||
val watchedFilmIds =
|
||||
filmLibraryRepository
|
||||
.findAll()
|
||||
.filter {
|
||||
it.userId == query.userId && it.isViewed
|
||||
}.map { it.filmId }
|
||||
.toSet()
|
||||
|
||||
return filmRepository
|
||||
.findAll()
|
||||
.asSequence()
|
||||
.filter { film -> query.contentType == null || film.contentType == query.contentType }
|
||||
.map { film ->
|
||||
scoreFilm(film, query.mood, preferences, ratings[film.id] != null, watchedFilmIds.contains(film.id))
|
||||
}.sortedByDescending { it.score }
|
||||
.take(query.limit.coerceAtLeast(1))
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun scoreFilm(
|
||||
film: Film,
|
||||
mood: String?,
|
||||
preferences: com.project.movienight.domain.model.UserPreferences?,
|
||||
hasUserRating: Boolean,
|
||||
watched: Boolean,
|
||||
): RecommendationResult {
|
||||
var score = 0.0
|
||||
val reasons = mutableListOf<String>()
|
||||
|
||||
preferences?.contentTypes?.let {
|
||||
if (it.isEmpty() || it.contains(film.contentType)) {
|
||||
score += 2.0
|
||||
reasons += "Matches content preference"
|
||||
}
|
||||
}
|
||||
|
||||
preferences?.weightedGenres?.forEach { (genre, weight) ->
|
||||
if (film.genres.any { it.equals(genre, ignoreCase = true) }) {
|
||||
score += weight
|
||||
reasons += "Matches genre $genre"
|
||||
}
|
||||
}
|
||||
|
||||
preferences?.castAndDirectors?.forEach { favorite ->
|
||||
val found =
|
||||
film.cast.any { it.equals(favorite, ignoreCase = true) } ||
|
||||
film.directors.any { it.equals(favorite, ignoreCase = true) }
|
||||
if (found) {
|
||||
score += 1.5
|
||||
reasons += "Matches favorite creator or cast member $favorite"
|
||||
}
|
||||
}
|
||||
|
||||
preferences?.moods?.forEach { preferredMood ->
|
||||
if (mood != null && preferredMood.equals(mood, ignoreCase = true)) {
|
||||
score += 1.25
|
||||
reasons += "Matches requested mood $mood"
|
||||
}
|
||||
}
|
||||
|
||||
film.imdbRating?.let {
|
||||
score += it / 2.0
|
||||
reasons += "Strong IMDb signal"
|
||||
}
|
||||
|
||||
film.platformRating?.let {
|
||||
score += it
|
||||
reasons += "Strong platform signal"
|
||||
}
|
||||
|
||||
if (hasUserRating) {
|
||||
score += 2.0
|
||||
reasons += "User has already rated similar content"
|
||||
}
|
||||
|
||||
if (watched) {
|
||||
score -= 3.0
|
||||
reasons += "Already watched"
|
||||
}
|
||||
|
||||
if (mood != null && film.title.contains(mood, ignoreCase = true)) {
|
||||
score += 0.5
|
||||
}
|
||||
|
||||
if (reasons.isEmpty()) {
|
||||
reasons += "Baseline recommendation from library catalog"
|
||||
}
|
||||
|
||||
return RecommendationResult(film = film, score = score, reasons = reasons)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.application.ports.input.GetUserPreferencesUseCase
|
||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesCommand
|
||||
import com.project.movienight.application.ports.input.UpsertUserPreferencesUseCase
|
||||
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
|
||||
import com.project.movienight.domain.model.UserPreferences
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class UserPreferencesService(
|
||||
private val userPreferencesRepository: UserPreferencesRepositoryPort,
|
||||
) : UpsertUserPreferencesUseCase,
|
||||
GetUserPreferencesUseCase {
|
||||
override fun upsert(command: UpsertUserPreferencesCommand): UserPreferences =
|
||||
userPreferencesRepository.save(
|
||||
UserPreferences(
|
||||
userId = command.userId,
|
||||
weightedGenres = command.weightedGenres,
|
||||
plotTypes = command.plotTypes,
|
||||
eras = command.eras,
|
||||
castAndDirectors = command.castAndDirectors,
|
||||
moods = command.moods,
|
||||
contentTypes = command.contentTypes,
|
||||
),
|
||||
)
|
||||
|
||||
override fun get(userId: java.util.UUID): UserPreferences? = userPreferencesRepository.findByUserId(userId)
|
||||
}
|
||||
Reference in New Issue
Block a user