Umbau zu SCS

This commit is contained in:
stefan
2025-07-17 15:17:31 +02:00
parent 67c52f7381
commit 029b0c86bc
255 changed files with 6458 additions and 26663 deletions
@@ -0,0 +1,68 @@
package at.mocode.dto.base
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Base DTO interface for all data transfer objects
*/
interface BaseDto
/**
* Base DTO for entities with ID and timestamps
*/
@Serializable
abstract class EntityDto : BaseDto {
@Serializable(with = UuidSerializer::class)
abstract val id: Uuid
@Serializable(with = KotlinInstantSerializer::class)
abstract val createdAt: Instant
@Serializable(with = KotlinInstantSerializer::class)
abstract val updatedAt: Instant
}
/**
* Standard API response wrapper
*/
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: ErrorDto? = null,
val message: String? = null
) : BaseDto
/**
* Error information DTO
*/
@Serializable
data class ErrorDto(
val code: String,
val message: String,
val details: Map<String, String>? = null
) : BaseDto
/**
* Pagination information
*/
@Serializable
data class PaginationDto(
val page: Int,
val size: Int,
val total: Long,
val totalPages: Int
) : BaseDto
/**
* Paginated response wrapper
*/
@Serializable
data class PagedResponse<T>(
val data: List<T>,
val pagination: PaginationDto
) : BaseDto
@@ -0,0 +1,35 @@
package at.mocode.enums
import kotlinx.serialization.Serializable
/**
* Data source enumeration - indicates where data originated from
*/
@Serializable
enum class DatenQuelleE { OEPS_ZNS, MANUELL }
/**
* Horse gender enumeration
*/
@Serializable
enum class PferdeGeschlechtE {
HENGST, STUTE, WALLACH, UNBEKANNT
}
/**
* Person gender enumeration
*/
@Serializable
enum class GeschlechtE { M, W, D, UNBEKANNT }
/**
* Sport discipline enumeration
*/
@Serializable
enum class SparteE { DRESSUR, SPRINGEN, VIELSEITIGKEIT, FAHREN, VOLTIGIEREN, WESTERN, DISTANZ, ISLAND, PFERDESPORT_SPIEL, BASIS, KOMBINIERT, SONSTIGES }
/**
* Venue/place type enumeration
*/
@Serializable
enum class PlatzTypE { AUSTRAGUNG, VORBEREITUNG, LONGIEREN, SONSTIGES }
@@ -0,0 +1,51 @@
package at.mocode.serializers
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object BigDecimalSerializer : KSerializer<BigDecimal> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigDecimal", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: BigDecimal) = encoder.encodeString(value.toStringExpanded())
override fun deserialize(decoder: Decoder): BigDecimal = BigDecimal.parseString(decoder.decodeString())
}
object UuidSerializer : KSerializer<Uuid> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uuid) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
}
object KotlinInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Instant = Instant.parse(decoder.decodeString())
}
object KotlinLocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalDate = LocalDate.parse(decoder.decodeString())
}
object KotlinLocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalDateTime = LocalDateTime.parse(decoder.decodeString())
}
object KotlinLocalTimeSerializer : KSerializer<LocalTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalTime) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): LocalTime = LocalTime.parse(decoder.decodeString())
}
@@ -0,0 +1,37 @@
package at.mocode.validation
import kotlinx.serialization.Serializable
/**
* Represents the result of a validation operation
*/
@Serializable
sealed class ValidationResult {
@Serializable
object Valid : ValidationResult()
@Serializable
data class Invalid(val errors: List<ValidationError>) : ValidationResult()
fun isValid(): Boolean = this is Valid
fun isInvalid(): Boolean = this is Invalid
}
/**
* Represents a single validation error
*/
@Serializable
data class ValidationError(
val field: String,
val message: String,
val code: String? = null
)
/**
* Exception thrown when validation fails
*/
class ValidationException(
val validationResult: ValidationResult.Invalid
) : IllegalArgumentException(
"Validation failed: ${validationResult.errors.joinToString(", ") { "${it.field}: ${it.message}" }}"
)
@@ -0,0 +1,150 @@
package at.mocode.validation
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
/**
* Common validation utilities
*/
object ValidationUtils {
/**
* Validates that a string is not blank
*/
fun validateNotBlank(value: String?, fieldName: String): ValidationError? {
return if (value.isNullOrBlank()) {
ValidationError(fieldName, "$fieldName cannot be blank", "REQUIRED")
} else null
}
/**
* Validates string length
*/
fun validateLength(value: String?, fieldName: String, maxLength: Int, minLength: Int = 0): ValidationError? {
if (value == null) return null
return when {
value.length < minLength -> ValidationError(
fieldName,
"$fieldName must be at least $minLength characters long",
"MIN_LENGTH"
)
value.length > maxLength -> ValidationError(
fieldName,
"$fieldName cannot exceed $maxLength characters",
"MAX_LENGTH"
)
else -> null
}
}
/**
* Validates email format
*/
fun validateEmail(email: String?, fieldName: String = "email"): ValidationError? {
if (email.isNullOrBlank()) return null
val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$".toRegex()
return if (!emailRegex.matches(email)) {
ValidationError(fieldName, "Invalid email format", "INVALID_FORMAT")
} else null
}
/**
* Validates phone number format (basic validation)
*/
fun validatePhoneNumber(phone: String?, fieldName: String = "telefon"): ValidationError? {
if (phone.isNullOrBlank()) return null
// Remove common separators and spaces
val cleanPhone = phone.replace(Regex("[\\s\\-\\(\\)\\+]"), "")
return if (cleanPhone.length < 6 || cleanPhone.length > 20 || !cleanPhone.all { it.isDigit() }) {
ValidationError(fieldName, "Invalid phone number format", "INVALID_FORMAT")
} else null
}
/**
* Validates postal code format (basic validation for various countries)
*/
fun validatePostalCode(postalCode: String?, fieldName: String = "plz"): ValidationError? {
if (postalCode.isNullOrBlank()) return null
// Basic validation: 3-10 alphanumeric characters
return if (postalCode.length < 3 || postalCode.length > 10 || !postalCode.all { it.isLetterOrDigit() }) {
ValidationError(fieldName, "Invalid postal code format", "INVALID_FORMAT")
} else null
}
/**
* Validates 3-letter country code
*/
fun validateCountryCode(countryCode: String?, fieldName: String = "nationalitaet"): ValidationError? {
if (countryCode.isNullOrBlank()) return null
return if (countryCode.length != 3 || !countryCode.all { it.isLetter() }) {
ValidationError(fieldName, "Country code must be exactly 3 letters", "INVALID_FORMAT")
} else null
}
/**
* Validates birth date
*/
fun validateBirthDate(birthDate: LocalDate?, fieldName: String = "geburtsdatum"): ValidationError? {
if (birthDate == null) return null
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
val minDate = LocalDate(1900, 1, 1)
return when {
birthDate > today -> ValidationError(
fieldName,
"Birth date cannot be in the future",
"FUTURE_DATE"
)
birthDate < minDate -> ValidationError(
fieldName,
"Birth date cannot be before year 1900",
"INVALID_DATE"
)
else -> null
}
}
/**
* Validates year value
*/
fun validateYear(year: Int?, fieldName: String, minYear: Int = 1900): ValidationError? {
if (year == null) return null
val currentYear = Clock.System.todayIn(TimeZone.currentSystemDefault()).year
return when {
year < minYear -> ValidationError(
fieldName,
"Year cannot be before $minYear",
"INVALID_YEAR"
)
year > currentYear + 10 -> ValidationError(
fieldName,
"Year cannot be more than 10 years in the future",
"FUTURE_YEAR"
)
else -> null
}
}
/**
* Validates OEPS Satz number format (Austrian specific)
*/
fun validateOepsSatzNr(oepsSatzNr: String?, fieldName: String = "oepsSatzNr"): ValidationError? {
if (oepsSatzNr.isNullOrBlank()) return null
// Basic validation: should be numeric and reasonable length
return if (oepsSatzNr.length < 3 || oepsSatzNr.length > 20 || !oepsSatzNr.all { it.isDigit() }) {
ValidationError(fieldName, "Invalid OEPS Satz number format", "INVALID_FORMAT")
} else null
}
}