Umbau zu SCS
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(project(":shared-kernel"))
|
||||
implementation(project(":member-management"))
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.uuid)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.kotlinDatetime)
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
implementation(libs.ktor.server.serializationKotlinxJson)
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
// Kotlin React dependencies with explicit versions
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:${libs.versions.kotlinWrappers.get()}")
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:${libs.versions.kotlinWrappers.get()}")
|
||||
|
||||
// NPM dependencies
|
||||
implementation(npm("react", "18.2.0"))
|
||||
implementation(npm("react-dom", "18.2.0"))
|
||||
implementation(npm("react-to-web-component", "2.0.2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package at.mocode.horses.application.usecase
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* 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.MANUAL
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for horse creation.
|
||||
*/
|
||||
data class CreateHorseResponse(
|
||||
val horse: DomPferd,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the horse creation use case.
|
||||
*
|
||||
* @param request The horse creation request data
|
||||
* @return CreateHorseResponse with the created horse or validation errors
|
||||
*/
|
||||
suspend fun execute(request: CreateHorseRequest): CreateHorseResponse {
|
||||
// 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 validationErrors = validateHorse(horse)
|
||||
if (validationErrors.isNotEmpty()) {
|
||||
return CreateHorseResponse(
|
||||
horse = horse,
|
||||
success = false,
|
||||
errors = validationErrors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for uniqueness constraints
|
||||
val uniquenessErrors = checkUniquenessConstraints(horse)
|
||||
if (uniquenessErrors.isNotEmpty()) {
|
||||
return CreateHorseResponse(
|
||||
horse = horse,
|
||||
success = false,
|
||||
errors = uniquenessErrors
|
||||
)
|
||||
}
|
||||
|
||||
// Save the horse
|
||||
val savedHorse = horseRepository.save(horse)
|
||||
|
||||
return CreateHorseResponse(
|
||||
horse = savedHorse,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the horse data according to business rules.
|
||||
*/
|
||||
private fun validateHorse(horse: DomPferd): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
// Use domain validation
|
||||
errors.addAll(horse.validateForRegistration())
|
||||
|
||||
// Additional business validations
|
||||
if (horse.stockmass != null && (horse.stockmass!! < 50 || horse.stockmass!! > 220)) {
|
||||
errors.add("Horse height must be between 50 and 220 cm")
|
||||
}
|
||||
|
||||
if (horse.geburtsdatum != null) {
|
||||
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
|
||||
if (horse.geburtsdatum!!.year > currentYear) {
|
||||
errors.add("Birth date cannot be in the future")
|
||||
}
|
||||
if (horse.geburtsdatum!!.year < (currentYear - 50)) {
|
||||
errors.add("Birth date cannot be more than 50 years ago")
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks uniqueness constraints for identification numbers.
|
||||
*/
|
||||
private suspend fun checkUniquenessConstraints(horse: DomPferd): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
// Check lebensnummer uniqueness
|
||||
horse.lebensnummer?.let { lebensnummer ->
|
||||
if (lebensnummer.isNotBlank() && horseRepository.existsByLebensnummer(lebensnummer)) {
|
||||
errors.add("A horse with this life number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check chip number uniqueness
|
||||
horse.chipNummer?.let { chipNummer ->
|
||||
if (chipNummer.isNotBlank() && horseRepository.existsByChipNummer(chipNummer)) {
|
||||
errors.add("A horse with this chip number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check passport number uniqueness
|
||||
horse.passNummer?.let { passNummer ->
|
||||
if (passNummer.isNotBlank() && horseRepository.existsByPassNummer(passNummer)) {
|
||||
errors.add("A horse with this passport number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check OEPS number uniqueness
|
||||
horse.oepsNummer?.let { oepsNummer ->
|
||||
if (oepsNummer.isNotBlank() && horseRepository.existsByOepsNummer(oepsNummer)) {
|
||||
errors.add("A horse with this OEPS number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check FEI number uniqueness
|
||||
horse.feiNummer?.let { feiNummer ->
|
||||
if (feiNummer.isNotBlank() && horseRepository.existsByFeiNummer(feiNummer)) {
|
||||
errors.add("A horse with this FEI number already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package at.mocode.horses.application.usecase
|
||||
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import com.benasher44.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
|
||||
}
|
||||
}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package at.mocode.horses.application.usecase
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
package at.mocode.horses.application.usecase
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* 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.MANUAL
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
if (horse.stockmass != null && (horse.stockmass!! < 50 || horse.stockmass!! > 220)) {
|
||||
errors.add("Horse height must be between 50 and 220 cm")
|
||||
}
|
||||
|
||||
// Birth date validation
|
||||
if (horse.geburtsdatum != null) {
|
||||
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
|
||||
if (horse.geburtsdatum!!.year > currentYear) {
|
||||
errors.add("Birth date cannot be in the future")
|
||||
}
|
||||
if (horse.geburtsdatum!!.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package at.mocode.horses.domain.model
|
||||
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import at.mocode.serializers.KotlinInstantSerializer
|
||||
import at.mocode.serializers.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Domain model representing a horse in the registry system.
|
||||
*
|
||||
* This entity contains all essential information about a horse including
|
||||
* identification, ownership, breeding information, and administrative data.
|
||||
* It serves as the core aggregate root for the horse-registry bounded context.
|
||||
*
|
||||
* @property pferdId Unique internal identifier for this horse (UUID).
|
||||
* @property pferdeName Name of the horse.
|
||||
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach).
|
||||
* @property geburtsdatum Birth date of the horse.
|
||||
* @property rasse Breed of the horse.
|
||||
* @property farbe Color/coat of the horse.
|
||||
* @property besitzerId ID of the current owner (Person from member-management context).
|
||||
* @property verantwortlichePersonId ID of the responsible person (trainer, rider, etc.).
|
||||
* @property zuechterName Name of the breeder.
|
||||
* @property zuchtbuchNummer Studbook number if registered.
|
||||
* @property lebensnummer Life number (unique identification number).
|
||||
* @property chipNummer Microchip number for identification.
|
||||
* @property passNummer Passport number.
|
||||
* @property oepsNummer OEPS (Austrian Equestrian Federation) number.
|
||||
* @property feiNummer FEI (International Equestrian Federation) number.
|
||||
* @property vaterName Name of the sire (father).
|
||||
* @property mutterName Name of the dam (mother).
|
||||
* @property mutterVaterName Name of the maternal grandsire.
|
||||
* @property stockmass Height of the horse in cm.
|
||||
* @property istAktiv Whether the horse is currently active in the system.
|
||||
* @property bemerkungen Additional notes or comments.
|
||||
* @property datenQuelle Source of the data (manual entry, import, etc.).
|
||||
* @property createdAt Timestamp when this record was created.
|
||||
* @property updatedAt Timestamp when this record was last updated.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomPferd(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val pferdId: Uuid = uuid4(),
|
||||
|
||||
// Basic Information
|
||||
var pferdeName: String,
|
||||
var geschlecht: PferdeGeschlechtE,
|
||||
var geburtsdatum: LocalDate? = null,
|
||||
var rasse: String? = null,
|
||||
var farbe: String? = null,
|
||||
|
||||
// Ownership and Responsibility
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var besitzerId: Uuid? = null,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var verantwortlichePersonId: Uuid? = null,
|
||||
|
||||
// Breeding Information
|
||||
var zuechterName: String? = null,
|
||||
var zuchtbuchNummer: String? = null,
|
||||
|
||||
// Identification Numbers
|
||||
var lebensnummer: String? = null,
|
||||
var chipNummer: String? = null,
|
||||
var passNummer: String? = null,
|
||||
var oepsNummer: String? = null,
|
||||
var feiNummer: String? = null,
|
||||
|
||||
// Pedigree Information
|
||||
var vaterName: String? = null,
|
||||
var mutterName: String? = null,
|
||||
var mutterVaterName: String? = null,
|
||||
|
||||
// Physical Characteristics
|
||||
var stockmass: Int? = null, // Height in cm
|
||||
|
||||
// Status and Administrative
|
||||
var istAktiv: Boolean = true,
|
||||
var bemerkungen: String? = null,
|
||||
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUAL,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Returns the display name for the horse, combining name and birth year if available.
|
||||
*/
|
||||
fun getDisplayName(): String {
|
||||
return if (geburtsdatum != null) {
|
||||
"$pferdeName (${geburtsdatum!!.year})"
|
||||
} else {
|
||||
pferdeName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the horse has complete identification information.
|
||||
*/
|
||||
fun hasCompleteIdentification(): Boolean {
|
||||
return !lebensnummer.isNullOrBlank() ||
|
||||
!chipNummer.isNullOrBlank() ||
|
||||
!passNummer.isNullOrBlank()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the horse is registered with OEPS.
|
||||
*/
|
||||
fun isOepsRegistered(): Boolean {
|
||||
return !oepsNummer.isNullOrBlank()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the horse is registered with FEI.
|
||||
*/
|
||||
fun isFeiRegistered(): Boolean {
|
||||
return !feiNummer.isNullOrBlank()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the age of the horse in years, or null if birth date is unknown.
|
||||
*/
|
||||
fun getAge(): Int? {
|
||||
return geburtsdatum?.let { birthDate ->
|
||||
val today = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
|
||||
today.year - birthDate.year - if (today.dayOfYear < birthDate.dayOfYear) 1 else 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that required fields are present for horse registration.
|
||||
*/
|
||||
fun validateForRegistration(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
if (pferdeName.isBlank()) {
|
||||
errors.add("Horse name is required")
|
||||
}
|
||||
|
||||
if (!hasCompleteIdentification()) {
|
||||
errors.add("At least one identification number (life number, chip number, or passport number) is required")
|
||||
}
|
||||
|
||||
if (besitzerId == null) {
|
||||
errors.add("Owner is required")
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this horse with updated timestamp.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomPferd {
|
||||
return this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package at.mocode.horses.domain.repository
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository interface for DomPferd (Horse) domain operations.
|
||||
*
|
||||
* This interface defines the contract for horse data access operations
|
||||
* without depending on specific implementation details (database, etc.).
|
||||
* Following the hexagonal architecture pattern, this interface belongs
|
||||
* to the domain layer and will be implemented in the infrastructure layer.
|
||||
*/
|
||||
interface HorseRepository {
|
||||
|
||||
/**
|
||||
* Finds a horse by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the horse
|
||||
* @return The horse if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): DomPferd?
|
||||
|
||||
/**
|
||||
* Finds a horse by its life number (Lebensnummer).
|
||||
*
|
||||
* @param lebensnummer The life number to search for
|
||||
* @return The horse if found, null otherwise
|
||||
*/
|
||||
suspend fun findByLebensnummer(lebensnummer: String): DomPferd?
|
||||
|
||||
/**
|
||||
* Finds a horse by its chip number.
|
||||
*
|
||||
* @param chipNummer The chip number to search for
|
||||
* @return The horse if found, null otherwise
|
||||
*/
|
||||
suspend fun findByChipNummer(chipNummer: String): DomPferd?
|
||||
|
||||
/**
|
||||
* Finds a horse by its passport number.
|
||||
*
|
||||
* @param passNummer The passport number to search for
|
||||
* @return The horse if found, null otherwise
|
||||
*/
|
||||
suspend fun findByPassNummer(passNummer: String): DomPferd?
|
||||
|
||||
/**
|
||||
* Finds a horse by its OEPS number.
|
||||
*
|
||||
* @param oepsNummer The OEPS number to search for
|
||||
* @return The horse if found, null otherwise
|
||||
*/
|
||||
suspend fun findByOepsNummer(oepsNummer: String): DomPferd?
|
||||
|
||||
/**
|
||||
* Finds a horse by its FEI number.
|
||||
*
|
||||
* @param feiNummer The FEI number to search for
|
||||
* @return The horse if found, null otherwise
|
||||
*/
|
||||
suspend fun findByFeiNummer(feiNummer: String): DomPferd?
|
||||
|
||||
/**
|
||||
* Finds horses by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against horse names
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching horses
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds all horses owned by a specific person.
|
||||
*
|
||||
* @param ownerId The ID of the owner (from member-management context)
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @return List of horses owned by the person
|
||||
*/
|
||||
suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds all horses for which a person is responsible.
|
||||
*
|
||||
* @param responsiblePersonId The ID of the responsible person
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @return List of horses for which the person is responsible
|
||||
*/
|
||||
suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean = true): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds horses by gender.
|
||||
*
|
||||
* @param geschlecht The gender to filter by
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of horses with the specified gender
|
||||
*/
|
||||
suspend fun findByGeschlecht(geschlecht: PferdeGeschlechtE, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds horses by breed.
|
||||
*
|
||||
* @param rasse The breed to filter by
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of horses of the specified breed
|
||||
*/
|
||||
suspend fun findByRasse(rasse: String, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds horses by birth year.
|
||||
*
|
||||
* @param birthYear The birth year to filter by
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @return List of horses born in the specified year
|
||||
*/
|
||||
suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean = true): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds 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
|
||||
* @return List of horses born within the specified year range
|
||||
*/
|
||||
suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean = true): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds all active horses.
|
||||
*
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of active horses
|
||||
*/
|
||||
suspend fun findAllActive(limit: Int = 1000): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds horses with OEPS registration.
|
||||
*
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @return List of OEPS registered horses
|
||||
*/
|
||||
suspend fun findOepsRegistered(activeOnly: Boolean = true): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Finds horses with FEI registration.
|
||||
*
|
||||
* @param activeOnly Whether to return only active horses
|
||||
* @return List of FEI registered horses
|
||||
*/
|
||||
suspend fun findFeiRegistered(activeOnly: Boolean = true): List<DomPferd>
|
||||
|
||||
/**
|
||||
* Saves a horse (create or update).
|
||||
*
|
||||
* @param horse The horse to save
|
||||
* @return The saved horse with updated timestamps
|
||||
*/
|
||||
suspend fun save(horse: DomPferd): DomPferd
|
||||
|
||||
/**
|
||||
* Deletes a horse by ID.
|
||||
*
|
||||
* @param id The unique identifier of the horse to delete
|
||||
* @return true if the horse was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Counts the total number of active horses.
|
||||
*
|
||||
* @return The total count of active horses
|
||||
*/
|
||||
suspend fun countActive(): Long
|
||||
|
||||
/**
|
||||
* Counts horses by owner.
|
||||
*
|
||||
* @param ownerId The ID of the owner
|
||||
* @param activeOnly Whether to count only active horses
|
||||
* @return The count of horses owned by the person
|
||||
*/
|
||||
suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): Long
|
||||
}
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
package at.mocode.horses.infrastructure.api
|
||||
|
||||
import at.mocode.horses.application.usecase.*
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.dto.base.BaseDto
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for horse registry operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for all horse-related operations
|
||||
* following REST conventions and proper HTTP status codes.
|
||||
*/
|
||||
class HorseController(
|
||||
private val horseRepository: HorseRepository
|
||||
) {
|
||||
|
||||
private val getHorseUseCase = GetHorseUseCase(horseRepository)
|
||||
private val createHorseUseCase = CreateHorseUseCase(horseRepository)
|
||||
private val updateHorseUseCase = UpdateHorseUseCase(horseRepository)
|
||||
private val deleteHorseUseCase = DeleteHorseUseCase(horseRepository)
|
||||
|
||||
/**
|
||||
* Configures the horse-related routes.
|
||||
*/
|
||||
fun configureRoutes(routing: Routing) {
|
||||
routing.route("/api/horses") {
|
||||
|
||||
// GET /api/horses - Get all horses with optional filtering
|
||||
get {
|
||||
try {
|
||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||
val limit = call.request.queryParameters["limit"]?.toInt() ?: 100
|
||||
val ownerId = call.request.queryParameters["ownerId"]?.let { uuidFrom(it) }
|
||||
val geschlecht = call.request.queryParameters["geschlecht"]?.let {
|
||||
PferdeGeschlechtE.valueOf(it)
|
||||
}
|
||||
val rasse = call.request.queryParameters["rasse"]
|
||||
val searchTerm = call.request.queryParameters["search"]
|
||||
|
||||
val horses = when {
|
||||
searchTerm != null -> horseRepository.findByName(searchTerm, limit)
|
||||
ownerId != null -> horseRepository.findByOwnerId(ownerId, activeOnly)
|
||||
geschlecht != null -> horseRepository.findByGeschlecht(geschlecht, activeOnly, limit)
|
||||
rasse != null -> horseRepository.findByRasse(rasse, activeOnly, limit)
|
||||
else -> horseRepository.findAllActive(limit)
|
||||
}
|
||||
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(horses))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to retrieve horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/{id} - Get horse by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val request = GetHorseUseCase.GetHorseRequest(horseId)
|
||||
val response = getHorseUseCase.execute(request)
|
||||
|
||||
if (response.success && response.horse != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(response.horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<Any>("Horse not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to retrieve horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/lebensnummer/{nummer} - Find by life number
|
||||
get("/search/lebensnummer/{nummer}") {
|
||||
try {
|
||||
val lebensnummer = call.parameters["nummer"]!!
|
||||
val horse = horseRepository.findByLebensnummer(lebensnummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<Any>("Horse with life number '$lebensnummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/chip/{nummer} - Find by chip number
|
||||
get("/search/chip/{nummer}") {
|
||||
try {
|
||||
val chipNummer = call.parameters["nummer"]!!
|
||||
val horse = horseRepository.findByChipNummer(chipNummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<Any>("Horse with chip number '$chipNummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/oeps-registered - Get OEPS registered horses
|
||||
get("/oeps-registered") {
|
||||
try {
|
||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||
val horses = horseRepository.findOepsRegistered(activeOnly)
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(horses))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to retrieve OEPS horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/fei-registered - Get FEI registered horses
|
||||
get("/fei-registered") {
|
||||
try {
|
||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||
val horses = horseRepository.findFeiRegistered(activeOnly)
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(horses))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to retrieve FEI horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/stats - Get horse statistics
|
||||
get("/stats") {
|
||||
try {
|
||||
val activeCount = horseRepository.countActive()
|
||||
val oepsCount = horseRepository.findOepsRegistered(true).size
|
||||
val feiCount = horseRepository.findFeiRegistered(true).size
|
||||
|
||||
val stats = HorseStats(
|
||||
totalActive = activeCount,
|
||||
oepsRegistered = oepsCount.toLong(),
|
||||
feiRegistered = feiCount.toLong()
|
||||
)
|
||||
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(stats))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to retrieve statistics: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/horses - Create new horse
|
||||
post {
|
||||
try {
|
||||
val createRequest = call.receive<CreateHorseUseCase.CreateHorseRequest>()
|
||||
val response = createHorseUseCase.execute(createRequest)
|
||||
|
||||
if (response.success) {
|
||||
call.respond(HttpStatusCode.Created, BaseDto.success(response.horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Validation failed", response.errors))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to create horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/horses/{id} - Update horse
|
||||
put("/{id}") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val updateData = call.receive<UpdateHorseRequest>()
|
||||
|
||||
val updateRequest = UpdateHorseUseCase.UpdateHorseRequest(
|
||||
pferdId = horseId,
|
||||
pferdeName = updateData.pferdeName,
|
||||
geschlecht = updateData.geschlecht,
|
||||
geburtsdatum = updateData.geburtsdatum,
|
||||
rasse = updateData.rasse,
|
||||
farbe = updateData.farbe,
|
||||
besitzerId = updateData.besitzerId,
|
||||
verantwortlichePersonId = updateData.verantwortlichePersonId,
|
||||
zuechterName = updateData.zuechterName,
|
||||
zuchtbuchNummer = updateData.zuchtbuchNummer,
|
||||
lebensnummer = updateData.lebensnummer,
|
||||
chipNummer = updateData.chipNummer,
|
||||
passNummer = updateData.passNummer,
|
||||
oepsNummer = updateData.oepsNummer,
|
||||
feiNummer = updateData.feiNummer,
|
||||
vaterName = updateData.vaterName,
|
||||
mutterName = updateData.mutterName,
|
||||
mutterVaterName = updateData.mutterVaterName,
|
||||
stockmass = updateData.stockmass,
|
||||
istAktiv = updateData.istAktiv,
|
||||
bemerkungen = updateData.bemerkungen,
|
||||
datenQuelle = updateData.datenQuelle
|
||||
)
|
||||
|
||||
val response = updateHorseUseCase.execute(updateRequest)
|
||||
|
||||
if (response.success && response.horse != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(response.horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Update failed", response.errors))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to update horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/horses/{id} - Delete horse
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val forceDelete = call.request.queryParameters["force"]?.toBoolean() ?: false
|
||||
|
||||
val deleteRequest = DeleteHorseUseCase.DeleteHorseRequest(horseId, forceDelete)
|
||||
val response = deleteHorseUseCase.execute(deleteRequest)
|
||||
|
||||
if (response.success) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success("Horse deleted successfully", response.warnings))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Delete failed", response.errors))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to delete horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/horses/{id}/soft-delete - Soft delete horse (mark as inactive)
|
||||
post("/{id}/soft-delete") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val response = deleteHorseUseCase.softDelete(horseId)
|
||||
|
||||
if (response.success) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success("Horse marked as inactive", response.warnings))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Soft delete failed", response.errors))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to soft delete horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/horses/batch-delete - Batch delete multiple horses
|
||||
post("/batch-delete") {
|
||||
try {
|
||||
val batchRequest = call.receive<BatchDeleteRequest>()
|
||||
val response = deleteHorseUseCase.batchDelete(batchRequest.horseIds, batchRequest.forceDelete)
|
||||
|
||||
val statusCode = if (response.overallSuccess) HttpStatusCode.OK else HttpStatusCode.PartialContent
|
||||
call.respond(statusCode, BaseDto.success(response))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<Any>("Failed to batch delete horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for updating horse data via API.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateHorseRequest(
|
||||
val pferdeName: String,
|
||||
val geschlecht: PferdeGeschlechtE,
|
||||
val geburtsdatum: kotlinx.datetime.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: at.mocode.enums.DatenQuelleE = at.mocode.enums.DatenQuelleE.MANUAL
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for batch delete request.
|
||||
*/
|
||||
@Serializable
|
||||
data class BatchDeleteRequest(
|
||||
val horseIds: List<Uuid>,
|
||||
val forceDelete: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for horse statistics.
|
||||
*/
|
||||
@Serializable
|
||||
data class HorseStats(
|
||||
val totalActive: Long,
|
||||
val oepsRegistered: Long,
|
||||
val feiRegistered: Long
|
||||
)
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
package at.mocode.horses.infrastructure.repository
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
|
||||
/**
|
||||
* PostgreSQL implementation of the HorseRepository using Exposed ORM.
|
||||
*
|
||||
* This implementation provides database operations for horse entities,
|
||||
* mapping between the domain model (DomPferd) and the database table (HorseTable).
|
||||
*/
|
||||
class HorseRepositoryImpl : HorseRepository {
|
||||
|
||||
override suspend fun findById(id: Uuid): DomPferd? {
|
||||
return HorseTable.select { HorseTable.id eq id }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? {
|
||||
return HorseTable.select { HorseTable.lebensnummer eq lebensnummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByChipNummer(chipNummer: String): DomPferd? {
|
||||
return HorseTable.select { HorseTable.chipNummer eq chipNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByPassNummer(passNummer: String): DomPferd? {
|
||||
return HorseTable.select { HorseTable.passNummer eq passNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? {
|
||||
return HorseTable.select { HorseTable.oepsNummer eq oepsNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? {
|
||||
return HorseTable.select { HorseTable.feiNummer eq feiNummer }
|
||||
.map { rowToDomPferd(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> {
|
||||
return HorseTable.select { HorseTable.pferdeName like "%$searchTerm%" }
|
||||
.orderBy(HorseTable.pferdeName)
|
||||
.limit(limit)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> {
|
||||
val query = HorseTable.select { HorseTable.besitzerId eq ownerId }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> {
|
||||
val query = HorseTable.select { HorseTable.verantwortlichePersonId eq responsiblePersonId }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByGeschlecht(geschlecht: PferdeGeschlechtE, activeOnly: Boolean, limit: Int): List<DomPferd> {
|
||||
val query = HorseTable.select { HorseTable.geschlecht eq geschlecht }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.limit(limit)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> {
|
||||
val query = HorseTable.select { HorseTable.rasse eq rasse }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.limit(limit)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> {
|
||||
val query = HorseTable.select {
|
||||
HorseTable.geburtsdatum.isNotNull() and
|
||||
HorseTable.geburtsdatum.year() eq birthYear
|
||||
}
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> {
|
||||
val query = HorseTable.select {
|
||||
HorseTable.geburtsdatum.isNotNull() and
|
||||
(HorseTable.geburtsdatum.year() greaterEq fromYear) and
|
||||
(HorseTable.geburtsdatum.year() lessEq toYear)
|
||||
}
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.geburtsdatum, SortOrder.DESC)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int): List<DomPferd> {
|
||||
return HorseTable.select { HorseTable.istAktiv eq true }
|
||||
.orderBy(HorseTable.pferdeName)
|
||||
.limit(limit)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> {
|
||||
val query = HorseTable.select { HorseTable.oepsNummer.isNotNull() }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> {
|
||||
val query = HorseTable.select { HorseTable.feiNummer.isNotNull() }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(HorseTable.pferdeName)
|
||||
.map { rowToDomPferd(it) }
|
||||
}
|
||||
|
||||
override suspend fun save(horse: DomPferd): DomPferd {
|
||||
val existingHorse = findById(horse.pferdId)
|
||||
|
||||
return if (existingHorse != null) {
|
||||
// Update existing horse
|
||||
val updatedHorse = horse.withUpdatedTimestamp()
|
||||
HorseTable.update({ HorseTable.id eq horse.pferdId }) {
|
||||
domPferdToStatement(it, updatedHorse)
|
||||
}
|
||||
updatedHorse
|
||||
} else {
|
||||
// Insert new horse
|
||||
HorseTable.insert {
|
||||
it[id] = horse.pferdId
|
||||
domPferdToStatement(it, horse)
|
||||
}
|
||||
horse
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean {
|
||||
val deletedRows = HorseTable.deleteWhere { HorseTable.id eq id }
|
||||
return deletedRows > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByLebensnummer(lebensnummer: String): Boolean {
|
||||
return HorseTable.select { HorseTable.lebensnummer eq lebensnummer }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByChipNummer(chipNummer: String): Boolean {
|
||||
return HorseTable.select { HorseTable.chipNummer eq chipNummer }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByPassNummer(passNummer: String): Boolean {
|
||||
return HorseTable.select { HorseTable.passNummer eq passNummer }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean {
|
||||
return HorseTable.select { HorseTable.oepsNummer eq oepsNummer }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByFeiNummer(feiNummer: String): Boolean {
|
||||
return HorseTable.select { HorseTable.feiNummer eq feiNummer }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long {
|
||||
return HorseTable.select { HorseTable.istAktiv eq true }
|
||||
.count()
|
||||
}
|
||||
|
||||
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long {
|
||||
val query = HorseTable.select { HorseTable.besitzerId eq ownerId }
|
||||
|
||||
return if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a database row to a DomPferd domain object.
|
||||
*/
|
||||
private fun rowToDomPferd(row: ResultRow): DomPferd {
|
||||
return DomPferd(
|
||||
pferdId = row[HorseTable.id].value,
|
||||
pferdeName = row[HorseTable.pferdeName],
|
||||
geschlecht = row[HorseTable.geschlecht],
|
||||
geburtsdatum = row[HorseTable.geburtsdatum],
|
||||
rasse = row[HorseTable.rasse],
|
||||
farbe = row[HorseTable.farbe],
|
||||
besitzerId = row[HorseTable.besitzerId],
|
||||
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
|
||||
zuechterName = row[HorseTable.zuechterName],
|
||||
zuchtbuchNummer = row[HorseTable.zuchtbuchNummer],
|
||||
lebensnummer = row[HorseTable.lebensnummer],
|
||||
chipNummer = row[HorseTable.chipNummer],
|
||||
passNummer = row[HorseTable.passNummer],
|
||||
oepsNummer = row[HorseTable.oepsNummer],
|
||||
feiNummer = row[HorseTable.feiNummer],
|
||||
vaterName = row[HorseTable.vaterName],
|
||||
mutterName = row[HorseTable.mutterName],
|
||||
mutterVaterName = row[HorseTable.mutterVaterName],
|
||||
stockmass = row[HorseTable.stockmass],
|
||||
istAktiv = row[HorseTable.istAktiv],
|
||||
bemerkungen = row[HorseTable.bemerkungen],
|
||||
datenQuelle = row[HorseTable.datenQuelle],
|
||||
createdAt = row[HorseTable.createdAt],
|
||||
updatedAt = row[HorseTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a DomPferd domain object to database statement values.
|
||||
*/
|
||||
private fun domPferdToStatement(statement: UpdateBuilder<*>, horse: DomPferd) {
|
||||
statement[HorseTable.pferdeName] = horse.pferdeName
|
||||
statement[HorseTable.geschlecht] = horse.geschlecht
|
||||
statement[HorseTable.geburtsdatum] = horse.geburtsdatum
|
||||
statement[HorseTable.rasse] = horse.rasse
|
||||
statement[HorseTable.farbe] = horse.farbe
|
||||
statement[HorseTable.besitzerId] = horse.besitzerId
|
||||
statement[HorseTable.verantwortlichePersonId] = horse.verantwortlichePersonId
|
||||
statement[HorseTable.zuechterName] = horse.zuechterName
|
||||
statement[HorseTable.zuchtbuchNummer] = horse.zuchtbuchNummer
|
||||
statement[HorseTable.lebensnummer] = horse.lebensnummer
|
||||
statement[HorseTable.chipNummer] = horse.chipNummer
|
||||
statement[HorseTable.passNummer] = horse.passNummer
|
||||
statement[HorseTable.oepsNummer] = horse.oepsNummer
|
||||
statement[HorseTable.feiNummer] = horse.feiNummer
|
||||
statement[HorseTable.vaterName] = horse.vaterName
|
||||
statement[HorseTable.mutterName] = horse.mutterName
|
||||
statement[HorseTable.mutterVaterName] = horse.mutterVaterName
|
||||
statement[HorseTable.stockmass] = horse.stockmass
|
||||
statement[HorseTable.istAktiv] = horse.istAktiv
|
||||
statement[HorseTable.bemerkungen] = horse.bemerkungen
|
||||
statement[HorseTable.datenQuelle] = horse.datenQuelle
|
||||
statement[HorseTable.createdAt] = horse.createdAt
|
||||
statement[HorseTable.updatedAt] = horse.updatedAt
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package at.mocode.horses.infrastructure.repository
|
||||
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import com.benasher44.uuid.Uuid
|
||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Database table definition for horses in the horse-registry context.
|
||||
*
|
||||
* This table stores all horse information including identification,
|
||||
* ownership, breeding data, and administrative information.
|
||||
*/
|
||||
object HorseTable : UUIDTable("horses") {
|
||||
// Basic Information
|
||||
val pferdeName = varchar("pferde_name", 255)
|
||||
val geschlecht = enumerationByName<PferdeGeschlechtE>("geschlecht", 20)
|
||||
val geburtsdatum = date("geburtsdatum").nullable()
|
||||
val rasse = varchar("rasse", 100).nullable()
|
||||
val farbe = varchar("farbe", 100).nullable()
|
||||
|
||||
// Ownership and Responsibility
|
||||
val besitzerId = uuid("besitzer_id").nullable()
|
||||
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
|
||||
|
||||
// Breeding Information
|
||||
val zuechterName = varchar("zuechter_name", 255).nullable()
|
||||
val zuchtbuchNummer = varchar("zuchtbuch_nummer", 100).nullable()
|
||||
|
||||
// Identification Numbers
|
||||
val lebensnummer = varchar("lebensnummer", 50).nullable()
|
||||
val chipNummer = varchar("chip_nummer", 50).nullable()
|
||||
val passNummer = varchar("pass_nummer", 50).nullable()
|
||||
val oepsNummer = varchar("oeps_nummer", 50).nullable()
|
||||
val feiNummer = varchar("fei_nummer", 50).nullable()
|
||||
|
||||
// Pedigree Information
|
||||
val vaterName = varchar("vater_name", 255).nullable()
|
||||
val mutterName = varchar("mutter_name", 255).nullable()
|
||||
val mutterVaterName = varchar("mutter_vater_name", 255).nullable()
|
||||
|
||||
// Physical Characteristics
|
||||
val stockmass = integer("stockmass").nullable()
|
||||
|
||||
// Status and Administrative
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val bemerkungen = text("bemerkungen").nullable()
|
||||
val datenQuelle = enumerationByName<DatenQuelleE>("daten_quelle", 20).default(DatenQuelleE.MANUAL)
|
||||
|
||||
// Audit Fields
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
init {
|
||||
// Indexes for performance
|
||||
index(false, pferdeName)
|
||||
index(false, besitzerId)
|
||||
index(false, lebensnummer)
|
||||
index(false, chipNummer)
|
||||
index(false, passNummer)
|
||||
index(false, oepsNummer)
|
||||
index(false, feiNummer)
|
||||
index(false, istAktiv)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user