(vision) SCS/DDD

This commit is contained in:
2025-07-18 23:07:05 +02:00
parent 029b0c86bc
commit 611e31e196
68 changed files with 6949 additions and 137 deletions
@@ -4,8 +4,13 @@ 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 at.mocode.dto.base.ApiResponse
import at.mocode.dto.base.ErrorDto
import at.mocode.validation.ValidationResult
import at.mocode.validation.ValidationError
import com.benasher44.uuid.Uuid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
/**
* Use case for creating a new horse in the registry.
@@ -40,25 +45,17 @@ class CreateHorseUseCase(
val mutterVaterName: String? = null,
val stockmass: Int? = null,
val bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUAL
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
)
/**
* 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
* @return ApiResponse with the created horse or validation errors
*/
suspend fun execute(request: CreateHorseRequest): CreateHorseResponse {
suspend fun execute(request: CreateHorseRequest): ApiResponse<DomPferd> {
// Create domain object
val horse = DomPferd(
pferdeName = request.pferdeName,
@@ -84,102 +81,126 @@ class CreateHorseUseCase(
)
// Validate the horse
val validationErrors = validateHorse(horse)
if (validationErrors.isNotEmpty()) {
return CreateHorseResponse(
horse = horse,
val validationResult = validateHorse(horse)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
errors = validationErrors
data = null,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Horse validation failed",
details = errors.associate { it.field to it.message }
)
)
}
// Check for uniqueness constraints
val uniquenessErrors = checkUniquenessConstraints(horse)
if (uniquenessErrors.isNotEmpty()) {
return CreateHorseResponse(
horse = horse,
val uniquenessResult = checkUniquenessConstraints(horse)
if (!uniquenessResult.isValid()) {
val errors = (uniquenessResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
errors = uniquenessErrors
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 CreateHorseResponse(
horse = savedHorse,
success = true
return ApiResponse(
success = true,
data = savedHorse,
message = "Horse created successfully"
)
}
/**
* Validates the horse data according to business rules.
*/
private fun validateHorse(horse: DomPferd): List<String> {
val errors = mutableListOf<String>()
private fun validateHorse(horse: DomPferd): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Use domain validation
errors.addAll(horse.validateForRegistration())
val domainErrors = horse.validateForRegistration()
domainErrors.forEach { errorMessage ->
errors.add(ValidationError("horse", errorMessage, "DOMAIN_VALIDATION"))
}
// Additional business validations
if (horse.stockmass != null && (horse.stockmass!! < 50 || horse.stockmass!! > 220)) {
errors.add("Horse height must be between 50 and 220 cm")
horse.stockmass?.let { height ->
if (height < 50 || height > 220) {
errors.add(ValidationError("stockmass", "Horse height must be between 50 and 220 cm", "INVALID_RANGE"))
}
}
if (horse.geburtsdatum != null) {
horse.geburtsdatum?.let { birthDate ->
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 (birthDate.year > currentYear) {
errors.add(ValidationError("geburtsdatum", "Birth date cannot be in the future", "FUTURE_DATE"))
}
if (horse.geburtsdatum!!.year < (currentYear - 50)) {
errors.add("Birth date cannot be more than 50 years ago")
if (birthDate.year < (currentYear - 50)) {
errors.add(ValidationError("geburtsdatum", "Birth date cannot be more than 50 years ago", "TOO_OLD"))
}
}
return errors
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Checks uniqueness constraints for identification numbers.
*/
private suspend fun checkUniquenessConstraints(horse: DomPferd): List<String> {
val errors = mutableListOf<String>()
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("A horse with this life number already exists")
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("A horse with this chip number already exists")
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("A horse with this passport number already exists")
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("A horse with this OEPS number already exists")
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("A horse with this FEI number already exists")
errors.add(ValidationError("feiNummer", "A horse with this FEI number already exists", "DUPLICATE"))
}
}
return errors
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
}
@@ -4,6 +4,7 @@ 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.todayIn
/**
* Use case for retrieving horse information.
@@ -6,6 +6,7 @@ import at.mocode.enums.PferdeGeschlechtE
import at.mocode.enums.DatenQuelleE
import com.benasher44.uuid.Uuid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
/**
* Use case for updating an existing horse in the registry.
@@ -42,7 +43,7 @@ class UpdateHorseUseCase(
val stockmass: Int? = null,
val istAktiv: Boolean = true,
val bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUAL
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
)
/**
@@ -135,17 +136,19 @@ class UpdateHorseUseCase(
}
// Height validation
if (horse.stockmass != null && (horse.stockmass!! < 50 || horse.stockmass!! > 220)) {
errors.add("Horse height must be between 50 and 220 cm")
horse.stockmass?.let { height ->
if (height < 50 || height > 220) {
errors.add("Horse height must be between 50 and 220 cm")
}
}
// Birth date validation
if (horse.geburtsdatum != null) {
horse.geburtsdatum?.let { birthDate ->
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
if (horse.geburtsdatum!!.year > currentYear) {
if (birthDate.year > currentYear) {
errors.add("Birth date cannot be in the future")
}
if (horse.geburtsdatum!!.year < (currentYear - 50)) {
if (birthDate.year < (currentYear - 50)) {
errors.add("Birth date cannot be more than 50 years ago")
}
}
@@ -9,6 +9,7 @@ import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
import kotlinx.serialization.Serializable
/**
@@ -83,7 +84,7 @@ data class DomPferd(
// Status and Administrative
var istAktiv: Boolean = true,
var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUAL,
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
// Audit Fields
@Serializable(with = KotlinInstantSerializer::class)
@@ -95,11 +96,9 @@ data class DomPferd(
* 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
}
return geburtsdatum?.let { birthDate ->
"$pferdeName (${birthDate.year})"
} ?: pferdeName
}
/**
@@ -131,7 +130,15 @@ data class DomPferd(
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
var age = today.year - birthDate.year
// Check if birthday has occurred this year
if (today.monthNumber < birthDate.monthNumber ||
(today.monthNumber == birthDate.monthNumber && today.dayOfMonth < birthDate.dayOfMonth)) {
age--
}
age
}
}
@@ -292,7 +292,7 @@ class HorseController(
val stockmass: Int? = null,
val istAktiv: Boolean = true,
val bemerkungen: String? = null,
val datenQuelle: at.mocode.enums.DatenQuelleE = at.mocode.enums.DatenQuelleE.MANUAL
val datenQuelle: at.mocode.enums.DatenQuelleE = at.mocode.enums.DatenQuelleE.MANUELL
)
/**
@@ -47,7 +47,7 @@ object HorseTable : UUIDTable("horses") {
// 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)
val datenQuelle = enumerationByName<DatenQuelleE>("daten_quelle", 20).default(DatenQuelleE.MANUELL)
// Audit Fields
val createdAt = timestamp("created_at")