(vision) SCS/DDD
This commit is contained in:
+64
-43
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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.
|
||||
|
||||
+9
-6
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user