Merge pull request #18

* MP-19 Refactoring: Einführung der "Registry" & "Masterdata" Trennung …

* MP-19 Refactoring: Frontend Tabula Rasa

* MP-19 Refactoring: Frontend Tabula Rasa

* refactoring:

* MP-20 fix(docker/clients): include `:domains` module in web/desktop b…

* MP-20 fix(web-app build): resolve JS compile error and add dev/prod b…

* MP-20 fix(web-app): remove vendor.js reference and harden JS bootstra…

* MP-20 fixing: clients

* MP-20 fixing: clients
This commit is contained in:
StefanMo
2025-11-30 14:13:12 +01:00
committed by GitHub
parent 596a05b69c
commit 9ea2b74a81
254 changed files with 5485 additions and 15971 deletions
@@ -0,0 +1,10 @@
plugins {
kotlin("jvm")
}
dependencies {
implementation(projects.horses.horsesDomain)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
testImplementation(projects.platform.platformTesting)
}
@@ -0,0 +1,207 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.horses.application.usecase
import at.mocode.horses.domain.model.DomPferd
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorDto
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
/**
* Use case for creating a new horse in the registry.
*
* This use case handles the business logic for horse registration including
* validation, uniqueness checks, and persistence.
*/
class CreateHorseUseCase(
private val horseRepository: HorseRepository
) {
/**
* Request data for creating a new horse.
*/
data class CreateHorseRequest(
val pferdeName: String,
val geschlecht: PferdeGeschlechtE,
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val besitzerId: Uuid? = null,
val verantwortlichePersonId: Uuid? = null,
val zuechterName: String? = null,
val zuchtbuchNummer: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val mutterVaterName: String? = null,
val stockmass: Int? = null,
val bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
)
/**
* Executes the horse creation use case.
*
* @param request The horse creation request data
* @return ApiResponse with the created horse or validation errors
*/
suspend fun execute(request: CreateHorseRequest): ApiResponse<DomPferd> {
// Create domain object
val horse = DomPferd(
pferdeName = request.pferdeName,
geschlecht = request.geschlecht,
geburtsdatum = request.geburtsdatum,
rasse = request.rasse,
farbe = request.farbe,
besitzerId = request.besitzerId,
verantwortlichePersonId = request.verantwortlichePersonId,
zuechterName = request.zuechterName,
zuchtbuchNummer = request.zuchtbuchNummer,
lebensnummer = request.lebensnummer,
chipNummer = request.chipNummer,
passNummer = request.passNummer,
oepsNummer = request.oepsNummer,
feiNummer = request.feiNummer,
vaterName = request.vaterName,
mutterName = request.mutterName,
mutterVaterName = request.mutterVaterName,
stockmass = request.stockmass,
bemerkungen = request.bemerkungen,
datenQuelle = request.datenQuelle
)
// Validate the horse
val validationResult = validateHorse(horse)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
data = null,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Horse validation failed",
details = errors.associate { it.field to it.message }
)
)
}
// Check for uniqueness constraints
val uniquenessResult = checkUniquenessConstraints(horse)
if (!uniquenessResult.isValid()) {
val errors = (uniquenessResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
data = null,
error = ErrorDto(
code = "UNIQUENESS_ERROR",
message = "Horse uniqueness validation failed",
details = errors.associate { it.field to it.message }
)
)
}
// Save the horse
val savedHorse = horseRepository.save(horse)
return ApiResponse(
success = true,
data = savedHorse,
message = "Horse created successfully"
)
}
/**
* Validates the horse data according to business rules.
*/
private fun validateHorse(horse: DomPferd): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Use domain validation
val domainErrors = horse.validateForRegistration()
domainErrors.forEach { errorMessage ->
errors.add(ValidationError("horse", errorMessage, "DOMAIN_VALIDATION"))
}
// Additional business validations
horse.stockmass?.let { height ->
if (height < 50 || height > 220) {
errors.add(ValidationError("stockmass", "Horse height must be between 50 and 220 cm", "INVALID_RANGE"))
}
}
horse.geburtsdatum?.let { birthDate ->
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
if (birthDate.year > currentYear) {
errors.add(ValidationError("geburtsdatum", "Birth date cannot be in the future", "FUTURE_DATE"))
}
if (birthDate.year < (currentYear - 50)) {
errors.add(ValidationError("geburtsdatum", "Birth date cannot be more than 50 years ago", "TOO_OLD"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Checks uniqueness constraints for identification numbers.
*/
private suspend fun checkUniquenessConstraints(horse: DomPferd): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Check lebensnummer uniqueness
horse.lebensnummer?.let { lebensnummer ->
if (lebensnummer.isNotBlank() && horseRepository.existsByLebensnummer(lebensnummer)) {
errors.add(ValidationError("lebensnummer", "A horse with this life number already exists", "DUPLICATE"))
}
}
// Check chip number uniqueness
horse.chipNummer?.let { chipNummer ->
if (chipNummer.isNotBlank() && horseRepository.existsByChipNummer(chipNummer)) {
errors.add(ValidationError("chipNummer", "A horse with this chip number already exists", "DUPLICATE"))
}
}
// Check passport number uniqueness
horse.passNummer?.let { passNummer ->
if (passNummer.isNotBlank() && horseRepository.existsByPassNummer(passNummer)) {
errors.add(ValidationError("passNummer", "A horse with this passport number already exists", "DUPLICATE"))
}
}
// Check OEPS number uniqueness
horse.oepsNummer?.let { oepsNummer ->
if (oepsNummer.isNotBlank() && horseRepository.existsByOepsNummer(oepsNummer)) {
errors.add(ValidationError("oepsNummer", "A horse with this OEPS number already exists", "DUPLICATE"))
}
}
// Check FEI number uniqueness
horse.feiNummer?.let { feiNummer ->
if (feiNummer.isNotBlank() && horseRepository.existsByFeiNummer(feiNummer)) {
errors.add(ValidationError("feiNummer", "A horse with this FEI number already exists", "DUPLICATE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
}
@@ -0,0 +1,215 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.horses.application.usecase
import at.mocode.horses.domain.repository.HorseRepository
import kotlin.uuid.Uuid
/**
* Use case for deleting a horse from the registry.
*
* This use case handles the business logic for horse deletion including
* existence checks and business rule validation.
*/
class DeleteHorseUseCase(
private val horseRepository: HorseRepository
) {
/**
* Request data for deleting a horse.
*/
data class DeleteHorseRequest(
val pferdId: Uuid,
val forceDelete: Boolean = false
)
/**
* Response data for horse deletion.
*/
data class DeleteHorseResponse(
val success: Boolean,
val errors: List<String> = emptyList(),
val warnings: List<String> = emptyList()
)
/**
* Executes the horse deletion use case.
*
* @param request The horse deletion request data
* @return DeleteHorseResponse indicating success or failure with errors
*/
suspend fun execute(request: DeleteHorseRequest): DeleteHorseResponse {
// Check if horse exists
val existingHorse = horseRepository.findById(request.pferdId)
?: return DeleteHorseResponse(
success = false,
errors = listOf("Horse not found")
)
// Check business rules for deletion
val businessRuleErrors = checkBusinessRules(request, existingHorse.pferdeName)
if (businessRuleErrors.isNotEmpty() && !request.forceDelete) {
return DeleteHorseResponse(
success = false,
errors = businessRuleErrors
)
}
// Generate warnings for important information
val warnings = generateWarnings(existingHorse.pferdeName, existingHorse.oepsNummer, existingHorse.feiNummer)
// Perform the deletion
val deleted = horseRepository.delete(request.pferdId)
return if (deleted) {
DeleteHorseResponse(
success = true,
warnings = warnings
)
} else {
DeleteHorseResponse(
success = false,
errors = listOf("Failed to delete horse from database")
)
}
}
/**
* Soft delete alternative - marks horse as inactive instead of deleting.
*/
suspend fun softDelete(pferdId: Uuid): DeleteHorseResponse {
val existingHorse = horseRepository.findById(pferdId)
?: return DeleteHorseResponse(
success = false,
errors = listOf("Horse not found")
)
if (!existingHorse.istAktiv) {
return DeleteHorseResponse(
success = false,
errors = listOf("Horse is already inactive")
)
}
// Mark as inactive
val inactiveHorse = existingHorse.copy(istAktiv = false).withUpdatedTimestamp()
horseRepository.save(inactiveHorse)
return DeleteHorseResponse(
success = true,
warnings = listOf("Horse marked as inactive instead of deleted")
)
}
/**
* Checks business rules that might prevent deletion.
*/
private suspend fun checkBusinessRules(request: DeleteHorseRequest, horseName: String): List<String> {
val errors = mutableListOf<String>()
// In a real system, you would check for:
// - Active competitions/entries
// - Historical records that should be preserved
// - Breeding records
// - License dependencies
// For now, we'll implement basic checks
// Example: Check if horse has OEPS or FEI registration
val horse = horseRepository.findById(request.pferdId)
if (horse != null) {
if (horse.isOepsRegistered() && !request.forceDelete) {
errors.add("Cannot delete OEPS registered horse without force delete flag")
}
if (horse.isFeiRegistered() && !request.forceDelete) {
errors.add("Cannot delete FEI registered horse without force delete flag")
}
// Check if horse has breeding information (might be important for pedigree)
if ((horse.vaterName != null || horse.mutterName != null) && !request.forceDelete) {
errors.add("Horse has pedigree information that might be referenced by other horses")
}
}
return errors
}
/**
* Generates warnings about the deletion.
*/
private fun generateWarnings(horseName: String, oepsNummer: String?, feiNummer: String?): List<String> {
val warnings = mutableListOf<String>()
warnings.add("Horse '$horseName' will be permanently deleted")
if (!oepsNummer.isNullOrBlank()) {
warnings.add("OEPS registration number '$oepsNummer' will be lost")
}
if (!feiNummer.isNullOrBlank()) {
warnings.add("FEI registration number '$feiNummer' will be lost")
}
warnings.add("This action cannot be undone")
return warnings
}
/**
* Batch delete multiple horses.
*/
suspend fun batchDelete(horseIds: List<Uuid>, forceDelete: Boolean = false): BatchDeleteResponse {
val results = mutableListOf<DeleteResult>()
var successCount = 0
var errorCount = 0
for (horseId in horseIds) {
val request = DeleteHorseRequest(horseId, forceDelete)
val response = execute(request)
results.add(
DeleteResult(
horseId = horseId,
success = response.success,
errors = response.errors,
warnings = response.warnings
)
)
if (response.success) {
successCount++
} else {
errorCount++
}
}
return BatchDeleteResponse(
results = results,
successCount = successCount,
errorCount = errorCount,
totalCount = horseIds.size
)
}
/**
* Result for individual horse deletion in batch operation.
*/
data class DeleteResult(
val horseId: Uuid,
val success: Boolean,
val errors: List<String> = emptyList(),
val warnings: List<String> = emptyList()
)
/**
* Response for batch delete operation.
*/
data class BatchDeleteResponse(
val results: List<DeleteResult>,
val successCount: Int,
val errorCount: Int,
val totalCount: Int
) {
val overallSuccess: Boolean = errorCount == 0
}
}
@@ -0,0 +1,304 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.horses.application.usecase
import at.mocode.horses.domain.model.DomPferd
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.core.domain.model.PferdeGeschlechtE
import kotlin.uuid.Uuid
import kotlinx.datetime.todayIn
/**
* Use case for retrieving horse information.
*
* This use case encapsulates the business logic for fetching horse data
* and provides a clean interface for the application layer.
*/
class GetHorseUseCase(
private val horseRepository: HorseRepository
) {
/**
* Retrieves a horse by its unique ID.
*
* @param horseId The unique identifier of the horse
* @return The horse if found, null otherwise
*/
suspend fun getById(horseId: Uuid): DomPferd? {
return horseRepository.findById(horseId)
}
/**
* Retrieves a horse by its life number.
*
* @param lebensnummer The life number to search for
* @return The horse if found, null otherwise
*/
suspend fun getByLebensnummer(lebensnummer: String): DomPferd? {
require(lebensnummer.isNotBlank()) { "Life number cannot be blank" }
return horseRepository.findByLebensnummer(lebensnummer.trim())
}
/**
* Retrieves a horse by its chip number.
*
* @param chipNummer The chip number to search for
* @return The horse if found, null otherwise
*/
suspend fun getByChipNummer(chipNummer: String): DomPferd? {
require(chipNummer.isNotBlank()) { "Chip number cannot be blank" }
return horseRepository.findByChipNummer(chipNummer.trim())
}
/**
* Retrieves a horse by its passport number.
*
* @param passNummer The passport number to search for
* @return The horse if found, null otherwise
*/
suspend fun getByPassNummer(passNummer: String): DomPferd? {
require(passNummer.isNotBlank()) { "Passport number cannot be blank" }
return horseRepository.findByPassNummer(passNummer.trim())
}
/**
* Retrieves a horse by its OEPS number.
*
* @param oepsNummer The OEPS number to search for
* @return The horse if found, null otherwise
*/
suspend fun getByOepsNummer(oepsNummer: String): DomPferd? {
require(oepsNummer.isNotBlank()) { "OEPS number cannot be blank" }
return horseRepository.findByOepsNummer(oepsNummer.trim())
}
/**
* Retrieves a horse by its FEI number.
*
* @param feiNummer The FEI number to search for
* @return The horse if found, null otherwise
*/
suspend fun getByFeiNummer(feiNummer: String): DomPferd? {
require(feiNummer.isNotBlank()) { "FEI number cannot be blank" }
return horseRepository.findByFeiNummer(feiNummer.trim())
}
/**
* Searches for horses by name (partial match).
*
* @param searchTerm The search term to match against horse names
* @param limit Maximum number of results to return (default: 50)
* @return List of matching horses
*/
suspend fun searchByName(searchTerm: String, limit: Int = 50): List<DomPferd> {
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
require(limit > 0) { "Limit must be positive" }
return horseRepository.findByName(searchTerm.trim(), limit)
}
/**
* Retrieves all horses owned by a specific person.
*
* @param ownerId The ID of the owner
* @param activeOnly Whether to return only active horses (default: true)
* @return List of horses owned by the person
*/
suspend fun getByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): List<DomPferd> {
return horseRepository.findByOwnerId(ownerId, activeOnly)
}
/**
* Retrieves all horses for which a person is responsible.
*
* @param responsiblePersonId The ID of the responsible person
* @param activeOnly Whether to return only active horses (default: true)
* @return List of horses for which the person is responsible
*/
suspend fun getByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean = true): List<DomPferd> {
return horseRepository.findByResponsiblePersonId(responsiblePersonId, activeOnly)
}
/**
* Retrieves horses by gender.
*
* @param geschlecht The gender to filter by
* @param activeOnly Whether to return only active horses (default: true)
* @param limit Maximum number of results to return (default: 100)
* @return List of horses with the specified gender
*/
suspend fun getByGeschlecht(geschlecht: PferdeGeschlechtE, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd> {
require(limit > 0) { "Limit must be positive" }
return horseRepository.findByGeschlecht(geschlecht, activeOnly, limit)
}
/**
* Retrieves horses by breed.
*
* @param rasse The breed to filter by
* @param activeOnly Whether to return only active horses (default: true)
* @param limit Maximum number of results to return (default: 100)
* @return List of horses of the specified breed
*/
suspend fun getByRasse(rasse: String, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd> {
require(rasse.isNotBlank()) { "Breed cannot be blank" }
require(limit > 0) { "Limit must be positive" }
return horseRepository.findByRasse(rasse.trim(), activeOnly, limit)
}
/**
* Retrieves horses by birth year.
*
* @param birthYear The birth year to filter by
* @param activeOnly Whether to return only active horses (default: true)
* @return List of horses born in the specified year
*/
suspend fun getByBirthYear(birthYear: Int, activeOnly: Boolean = true): List<DomPferd> {
require(birthYear > 1900) { "Birth year must be after 1900" }
require(birthYear <= kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year) {
"Birth year cannot be in the future"
}
return horseRepository.findByBirthYear(birthYear, activeOnly)
}
/**
* Retrieves horses by birth year range.
*
* @param fromYear The start year (inclusive)
* @param toYear The end year (inclusive)
* @param activeOnly Whether to return only active horses (default: true)
* @return List of horses born within the specified year range
*/
suspend fun getByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean = true): List<DomPferd> {
require(fromYear > 1900) { "From year must be after 1900" }
require(toYear >= fromYear) { "To year must be greater than or equal to from year" }
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
require(toYear <= currentYear) { "To year cannot be in the future" }
return horseRepository.findByBirthYearRange(fromYear, toYear, activeOnly)
}
/**
* Retrieves all active horses.
*
* @param limit Maximum number of results to return (default: 1000)
* @return List of active horses
*/
suspend fun getAllActive(limit: Int = 1000): List<DomPferd> {
require(limit > 0) { "Limit must be positive" }
return horseRepository.findAllActive(limit)
}
/**
* Retrieves horses with OEPS registration.
*
* @param activeOnly Whether to return only active horses (default: true)
* @return List of OEPS registered horses
*/
suspend fun getOepsRegistered(activeOnly: Boolean = true): List<DomPferd> {
return horseRepository.findOepsRegistered(activeOnly)
}
/**
* Retrieves horses with FEI registration.
*
* @param activeOnly Whether to return only active horses (default: true)
* @return List of FEI registered horses
*/
suspend fun getFeiRegistered(activeOnly: Boolean = true): List<DomPferd> {
return horseRepository.findFeiRegistered(activeOnly)
}
/**
* Checks if a horse with the given life number exists.
*
* @param lebensnummer The life number to check
* @return true if a horse with this life number exists, false otherwise
*/
suspend fun existsByLebensnummer(lebensnummer: String): Boolean {
require(lebensnummer.isNotBlank()) { "Life number cannot be blank" }
return horseRepository.existsByLebensnummer(lebensnummer.trim())
}
/**
* Checks if a horse with the given chip number exists.
*
* @param chipNummer The chip number to check
* @return true if a horse with this chip number exists, false otherwise
*/
suspend fun existsByChipNummer(chipNummer: String): Boolean {
require(chipNummer.isNotBlank()) { "Chip number cannot be blank" }
return horseRepository.existsByChipNummer(chipNummer.trim())
}
/**
* Checks if a horse with the given passport number exists.
*
* @param passNummer The passport number to check
* @return true if a horse with this passport number exists, false otherwise
*/
suspend fun existsByPassNummer(passNummer: String): Boolean {
require(passNummer.isNotBlank()) { "Passport number cannot be blank" }
return horseRepository.existsByPassNummer(passNummer.trim())
}
/**
* Checks if a horse with the given OEPS number exists.
*
* @param oepsNummer The OEPS number to check
* @return true if a horse with this OEPS number exists, false otherwise
*/
suspend fun existsByOepsNummer(oepsNummer: String): Boolean {
require(oepsNummer.isNotBlank()) { "OEPS number cannot be blank" }
return horseRepository.existsByOepsNummer(oepsNummer.trim())
}
/**
* Checks if a horse with the given FEI number exists.
*
* @param feiNummer The FEI number to check
* @return true if a horse with this FEI number exists, false otherwise
*/
suspend fun existsByFeiNummer(feiNummer: String): Boolean {
require(feiNummer.isNotBlank()) { "FEI number cannot be blank" }
return horseRepository.existsByFeiNummer(feiNummer.trim())
}
/**
* Counts the total number of active horses.
*
* @return The total count of active horses
*/
suspend fun countActive(): Long {
return horseRepository.countActive()
}
/**
* Counts horses by owner.
*
* @param ownerId The ID of the owner
* @param activeOnly Whether to count only active horses (default: true)
* @return The count of horses owned by the person
*/
suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): Long {
return horseRepository.countByOwnerId(ownerId, activeOnly)
}
/**
* Counts horses with OEPS registration.
*
* @param activeOnly Whether to count only active horses (default: true)
* @return The count of OEPS registered horses
*/
suspend fun countOepsRegistered(activeOnly: Boolean = true): Long {
return horseRepository.countOepsRegistered(activeOnly)
}
/**
* Counts horses with FEI registration.
*
* @param activeOnly Whether to count only active horses (default: true)
* @return The count of FEI registered horses
*/
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long {
return horseRepository.countFeiRegistered(activeOnly)
}
}
@@ -0,0 +1,256 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.horses.application.usecase
import at.mocode.horses.domain.model.DomPferd
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorDto
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import at.mocode.core.utils.database.DatabaseFactory
import kotlin.uuid.Uuid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
/**
* Transactional version of CreateHorseUseCase that ensures all database operations
* run within a single transaction to maintain data consistency.
*
* This use case handles the business logic for horse registration including
* validation, uniqueness checks, and persistence - all within a single transaction.
*/
class TransactionalCreateHorseUseCase(
private val horseRepository: HorseRepository
) {
/**
* Request data for creating a new horse.
*/
data class CreateHorseRequest(
val pferdeName: String,
val geschlecht: PferdeGeschlechtE,
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val besitzerId: Uuid? = null,
val verantwortlichePersonId: Uuid? = null,
val zuechterName: String? = null,
val zuchtbuchNummer: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val mutterVaterName: String? = null,
val stockmass: Int? = null,
val bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
)
/**
* Executes the horse creation use case within a single transaction.
*
* @param request The horse creation request data
* @return ApiResponse with the created horse or validation errors
*/
suspend fun execute(request: CreateHorseRequest): ApiResponse<DomPferd> {
println("[DEBUG_LOG] TransactionalCreateHorseUseCase.execute() called for horse: ${request.pferdeName}")
// Wrap the entire use case logic in a single transaction
return DatabaseFactory.dbQuery {
println("[DEBUG_LOG] Inside transaction for horse: ${request.pferdeName}")
// Create domain object
val horse = DomPferd(
pferdeName = request.pferdeName,
geschlecht = request.geschlecht,
geburtsdatum = request.geburtsdatum,
rasse = request.rasse,
farbe = request.farbe,
besitzerId = request.besitzerId,
verantwortlichePersonId = request.verantwortlichePersonId,
zuechterName = request.zuechterName,
zuchtbuchNummer = request.zuchtbuchNummer,
lebensnummer = request.lebensnummer,
chipNummer = request.chipNummer,
passNummer = request.passNummer,
oepsNummer = request.oepsNummer,
feiNummer = request.feiNummer,
vaterName = request.vaterName,
mutterName = request.mutterName,
mutterVaterName = request.mutterVaterName,
stockmass = request.stockmass,
bemerkungen = request.bemerkungen,
datenQuelle = request.datenQuelle
)
// Validate the horse
println("[DEBUG_LOG] Starting validation for horse: ${horse.pferdeName}")
val validationResult = validateHorse(horse)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors
println("[DEBUG_LOG] Validation failed for horse: ${horse.pferdeName}, errors: ${errors.map { "${it.field}: ${it.message}" }}")
return@dbQuery ApiResponse(
success = false,
data = null,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Horse validation failed",
details = errors.associate { it.field to it.message }
)
)
}
println("[DEBUG_LOG] Validation passed for horse: ${horse.pferdeName}")
// Check for uniqueness constraints - all within the same transaction
println("[DEBUG_LOG] Starting uniqueness check for horse: ${horse.pferdeName}")
val uniquenessResult = checkUniquenessConstraints(horse)
if (!uniquenessResult.isValid()) {
val errors = (uniquenessResult as ValidationResult.Invalid).errors
println("[DEBUG_LOG] Uniqueness check failed for horse: ${horse.pferdeName}, errors: ${errors.map { "${it.field}: ${it.message}" }}")
return@dbQuery ApiResponse(
success = false,
data = null,
error = ErrorDto(
code = "UNIQUENESS_ERROR",
message = "Horse uniqueness validation failed",
details = errors.associate { it.field to it.message }
)
)
}
println("[DEBUG_LOG] Uniqueness check passed for horse: ${horse.pferdeName}")
// Save the horse - still within the same transaction
println("[DEBUG_LOG] Saving horse: ${horse.pferdeName}")
try {
val savedHorse = horseRepository.save(horse)
println("[DEBUG_LOG] Horse saved successfully: ${savedHorse.pferdeName} with ID: ${savedHorse.pferdId}")
ApiResponse(
success = true,
data = savedHorse,
message = "Horse created successfully"
)
} catch (e: Exception) {
println("[DEBUG_LOG] Database constraint violation for horse: ${horse.pferdeName}, error: ${e.message}")
// Handle database constraint violations (duplicate keys)
if (e.message?.contains("unique", ignoreCase = true) == true ||
e.message?.contains("duplicate", ignoreCase = true) == true) {
// Determine which field caused the constraint violation
val constraintField = when {
e.message?.contains("lebensnummer", ignoreCase = true) == true -> "lebensnummer"
e.message?.contains("chip_nummer", ignoreCase = true) == true -> "chipNummer"
e.message?.contains("pass_nummer", ignoreCase = true) == true -> "passNummer"
e.message?.contains("oeps_nummer", ignoreCase = true) == true -> "oepsNummer"
e.message?.contains("fei_nummer", ignoreCase = true) == true -> "feiNummer"
else -> "identification"
}
ApiResponse(
success = false,
data = null,
error = ErrorDto(
code = "UNIQUENESS_ERROR",
message = "Horse uniqueness validation failed due to database constraint",
details = mapOf(constraintField to "A horse with this ${constraintField} already exists")
)
)
} else {
// Re-throw other exceptions
throw e
}
}
}
}
/**
* Validates the horse data according to business rules.
*/
private fun validateHorse(horse: DomPferd): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Use domain validation
val domainErrors = horse.validateForRegistration()
domainErrors.forEach { errorMessage ->
errors.add(ValidationError("horse", errorMessage, "DOMAIN_VALIDATION"))
}
// Additional business validations
horse.stockmass?.let { height ->
if (height < 50 || height > 220) {
errors.add(ValidationError("stockmass", "Horse height must be between 50 and 220 cm", "INVALID_RANGE"))
}
}
horse.geburtsdatum?.let { birthDate ->
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
if (birthDate.year > currentYear) {
errors.add(ValidationError("geburtsdatum", "Birth date cannot be in the future", "FUTURE_DATE"))
}
if (birthDate.year < (currentYear - 50)) {
errors.add(ValidationError("geburtsdatum", "Birth date cannot be more than 50 years ago", "TOO_OLD"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Checks uniqueness constraints for identification numbers.
* Note: This method is called within a transaction, so all repository calls
* will use the same transaction context.
*/
private suspend fun checkUniquenessConstraints(horse: DomPferd): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Check lebensnummer uniqueness
horse.lebensnummer?.let { lebensnummer ->
if (lebensnummer.isNotBlank() && horseRepository.existsByLebensnummer(lebensnummer)) {
errors.add(ValidationError("lebensnummer", "A horse with this life number already exists", "DUPLICATE"))
}
}
// Check chip number uniqueness
horse.chipNummer?.let { chipNummer ->
if (chipNummer.isNotBlank() && horseRepository.existsByChipNummer(chipNummer)) {
errors.add(ValidationError("chipNummer", "A horse with this chip number already exists", "DUPLICATE"))
}
}
// Check passport number uniqueness
horse.passNummer?.let { passNummer ->
if (passNummer.isNotBlank() && horseRepository.existsByPassNummer(passNummer)) {
errors.add(ValidationError("passNummer", "A horse with this passport number already exists", "DUPLICATE"))
}
}
// Check OEPS number uniqueness
horse.oepsNummer?.let { oepsNummer ->
if (oepsNummer.isNotBlank() && horseRepository.existsByOepsNummer(oepsNummer)) {
errors.add(ValidationError("oepsNummer", "A horse with this OEPS number already exists", "DUPLICATE"))
}
}
// Check FEI number uniqueness
horse.feiNummer?.let { feiNummer ->
if (feiNummer.isNotBlank() && horseRepository.existsByFeiNummer(feiNummer)) {
errors.add(ValidationError("feiNummer", "A horse with this FEI number already exists", "DUPLICATE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
}
@@ -0,0 +1,213 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.horses.application.usecase
import at.mocode.horses.domain.model.DomPferd
import at.mocode.horses.domain.repository.HorseRepository
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.domain.model.DatenQuelleE
import kotlin.uuid.Uuid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
/**
* Use case for updating an existing horse in the registry.
*
* This use case handles the business logic for horse updates including
* validation, uniqueness checks, and persistence.
*/
class UpdateHorseUseCase(
private val horseRepository: HorseRepository
) {
/**
* Request data for updating a horse.
*/
data class UpdateHorseRequest(
val pferdId: Uuid,
val pferdeName: String,
val geschlecht: PferdeGeschlechtE,
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val besitzerId: Uuid? = null,
val verantwortlichePersonId: Uuid? = null,
val zuechterName: String? = null,
val zuchtbuchNummer: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val mutterVaterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean = true,
val bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
)
/**
* Response data for horse update.
*/
data class UpdateHorseResponse(
val horse: DomPferd?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Executes the horse update use case.
*
* @param request The horse update request data
* @return UpdateHorseResponse with the updated horse or validation errors
*/
suspend fun execute(request: UpdateHorseRequest): UpdateHorseResponse {
// Check if horse exists
val existingHorse = horseRepository.findById(request.pferdId)
?: return UpdateHorseResponse(
horse = null,
success = false,
errors = listOf("Horse not found")
)
// Create updated domain object
val updatedHorse = existingHorse.copy(
pferdeName = request.pferdeName,
geschlecht = request.geschlecht,
geburtsdatum = request.geburtsdatum,
rasse = request.rasse,
farbe = request.farbe,
besitzerId = request.besitzerId,
verantwortlichePersonId = request.verantwortlichePersonId,
zuechterName = request.zuechterName,
zuchtbuchNummer = request.zuchtbuchNummer,
lebensnummer = request.lebensnummer,
chipNummer = request.chipNummer,
passNummer = request.passNummer,
oepsNummer = request.oepsNummer,
feiNummer = request.feiNummer,
vaterName = request.vaterName,
mutterName = request.mutterName,
mutterVaterName = request.mutterVaterName,
stockmass = request.stockmass,
istAktiv = request.istAktiv,
bemerkungen = request.bemerkungen,
datenQuelle = request.datenQuelle
)
// Validate the updated horse
val validationErrors = validateHorse(updatedHorse)
if (validationErrors.isNotEmpty()) {
return UpdateHorseResponse(
horse = updatedHorse,
success = false,
errors = validationErrors
)
}
// Check for uniqueness constraints (excluding current horse)
val uniquenessErrors = checkUniquenessConstraints(updatedHorse, existingHorse)
if (uniquenessErrors.isNotEmpty()) {
return UpdateHorseResponse(
horse = updatedHorse,
success = false,
errors = uniquenessErrors
)
}
// Save the updated horse
val savedHorse = horseRepository.save(updatedHorse)
return UpdateHorseResponse(
horse = savedHorse,
success = true
)
}
/**
* Validates the horse data according to business rules.
*/
private fun validateHorse(horse: DomPferd): List<String> {
val errors = mutableListOf<String>()
// Basic validation
if (horse.pferdeName.isBlank()) {
errors.add("Horse name is required")
}
// Height validation
horse.stockmass?.let { height ->
if (height < 50 || height > 220) {
errors.add("Horse height must be between 50 and 220 cm")
}
}
// Birth date validation
horse.geburtsdatum?.let { birthDate ->
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
if (birthDate.year > currentYear) {
errors.add("Birth date cannot be in the future")
}
if (birthDate.year < (currentYear - 50)) {
errors.add("Birth date cannot be more than 50 years ago")
}
}
return errors
}
/**
* Checks uniqueness constraints for identification numbers, excluding the current horse.
*/
private suspend fun checkUniquenessConstraints(updatedHorse: DomPferd, existingHorse: DomPferd): List<String> {
val errors = mutableListOf<String>()
// Check lebensnummer uniqueness (if changed)
updatedHorse.lebensnummer?.let { lebensnummer ->
if (lebensnummer.isNotBlank() &&
lebensnummer != existingHorse.lebensnummer &&
horseRepository.existsByLebensnummer(lebensnummer)) {
errors.add("A horse with this life number already exists")
}
}
// Check chip number uniqueness (if changed)
updatedHorse.chipNummer?.let { chipNummer ->
if (chipNummer.isNotBlank() &&
chipNummer != existingHorse.chipNummer &&
horseRepository.existsByChipNummer(chipNummer)) {
errors.add("A horse with this chip number already exists")
}
}
// Check passport number uniqueness (if changed)
updatedHorse.passNummer?.let { passNummer ->
if (passNummer.isNotBlank() &&
passNummer != existingHorse.passNummer &&
horseRepository.existsByPassNummer(passNummer)) {
errors.add("A horse with this passport number already exists")
}
}
// Check OEPS number uniqueness (if changed)
updatedHorse.oepsNummer?.let { oepsNummer ->
if (oepsNummer.isNotBlank() &&
oepsNummer != existingHorse.oepsNummer &&
horseRepository.existsByOepsNummer(oepsNummer)) {
errors.add("A horse with this OEPS number already exists")
}
}
// Check FEI number uniqueness (if changed)
updatedHorse.feiNummer?.let { feiNummer ->
if (feiNummer.isNotBlank() &&
feiNummer != existingHorse.feiNummer &&
horseRepository.existsByFeiNummer(feiNummer)) {
errors.add("A horse with this FEI number already exists")
}
}
return errors
}
}