core: restore compilation and tests (ports, DTOs, repo fixes, metrics)

This commit is contained in:
ITQ
2026-05-20 16:57:19 +03:00
parent ea7e66c1a0
commit 5f2a6d12ac
18 changed files with 766 additions and 36 deletions
@@ -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)
}