einige Ergänzungen

This commit is contained in:
2025-07-25 23:16:16 +02:00
parent 4c382e64a5
commit 7e0b56a247
70 changed files with 7795 additions and 1894 deletions
@@ -280,4 +280,24 @@ class GetHorseUseCase(
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,255 @@
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.utils.validation.ValidationResult
import at.mocode.core.utils.validation.ValidationError
import at.mocode.core.utils.database.DatabaseFactory
import com.benasher44.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)
}
}
}