chore: remove deprecated horses, clubs, officials, and persons services

- Deleted obsolete modules related to horses, clubs, officials, and persons services, including their configurations, build files, and database provisioning scripts.
- Cleaned up associated references in the project structure (e.g., `settings.gradle.kts`).
- Removed unused database tables and Spring beans related to these domains.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-28 16:50:49 +01:00
parent 2cb3f0b125
commit c806660685
181 changed files with 4121 additions and 8694 deletions
@@ -1,11 +1,7 @@
plugins {
// KORREKTUR: Alle Plugins werden jetzt konsistent über den Version Catalog geladen.
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ktor)
application
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSerialization)
id("application")
}
application {
@@ -16,8 +12,8 @@ dependencies {
api(platform(libs.spring.boot.dependencies))
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.masterdata.masterdataDomain)
implementation(projects.masterdata.masterdataApplication)
implementation(projects.backend.services.masterdata.masterdataDomain)
implementation(projects.backend.services.masterdata.masterdataCommon)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
@@ -1,10 +1,14 @@
package at.mocode.masterdata.api
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorCode
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
import org.slf4j.LoggerFactory
private val logger = LoggerFactory.getLogger("at.mocode.masterdata.api.StatusPages")
// Eine einfache, eigene Exception, um "Nicht gefunden"-Fälle klarer zu machen.
class NotFoundException(message: String) : RuntimeException(message)
@@ -15,10 +19,10 @@ fun Application.configureStatusPages() {
// Regel 1: Fange alle "IllegalArgumentException" ab.
// Das passiert bei ungültigen Eingaben, z.B. ein falsches UUID-Format.
exception<IllegalArgumentException> { call, cause ->
log.warn("Bad Request: ${cause.message}")
val errorResponse = ApiResponse<Unit>(
message = cause.message ?: "Invalid input provided.",
errors = listOf("BAD_REQUEST")
logger.warn("Bad Request: ${cause.message}")
val errorResponse = ApiResponse.error<Unit>(
code = ErrorCode("BAD_REQUEST"),
message = cause.message ?: "Invalid input provided."
)
call.respond(HttpStatusCode.BadRequest, errorResponse)
}
@@ -26,10 +30,10 @@ fun Application.configureStatusPages() {
// Regel 2: Fange unsere eigene "NotFoundException" ab.
// Diese werfen wir, wenn eine Entität nicht in der DB gefunden wurde.
exception<NotFoundException> { call, cause ->
log.info("Resource not found: ${cause.message}")
val errorResponse = ApiResponse<Unit>(
message = cause.message ?: "The requested resource was not found.",
errors = listOf("NOT_FOUND")
logger.info("Resource not found: ${cause.message}")
val errorResponse = ApiResponse.error<Unit>(
code = ErrorCode("NOT_FOUND"),
message = cause.message ?: "The requested resource was not found."
)
call.respond(HttpStatusCode.NotFound, errorResponse)
}
@@ -37,10 +41,10 @@ fun Application.configureStatusPages() {
// Regel 3: Fange alle anderen, unerwarteten Fehler ab.
// Das ist unser Sicherheitsnetz für alles, was wir nicht vorhergesehen haben.
exception<Throwable> { call, cause ->
log.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
val errorResponse = ApiResponse<Unit>(
message = "An unexpected internal server error occurred.",
errors = listOf("INTERNAL_SERVER_ERROR")
logger.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
val errorResponse = ApiResponse.error<Unit>(
code = ErrorCode("INTERNAL_SERVER_ERROR"),
message = "An unexpected internal server error occurred."
)
call.respond(HttpStatusCode.InternalServerError, errorResponse)
}
@@ -1,4 +1,5 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
@@ -6,7 +7,6 @@ import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.application.usecase.CreateAltersklasseUseCase
import at.mocode.masterdata.application.usecase.GetAltersklasseUseCase
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.core.utils.validation.ApiValidationUtils
import kotlin.uuid.Uuid
import io.ktor.http.*
import io.ktor.server.request.*
@@ -16,449 +16,220 @@ import kotlinx.serialization.Serializable
/**
* REST API controller for age class management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* age class functionality, following REST conventions and proper error handling.
*/
class AltersklasseController(
private val getAltersklasseUseCase: GetAltersklasseUseCase,
private val createAltersklasseUseCase: CreateAltersklasseUseCase
private val getAltersklasseUseCase: GetAltersklasseUseCase,
private val createAltersklasseUseCase: CreateAltersklasseUseCase
) {
/**
* DTO for age class API responses.
*/
@Serializable
data class AltersklasseDto(
val altersklasseId: String,
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = null,
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true,
val createdAt: String,
val updatedAt: String
)
@Serializable
data class AltersklasseDto(
val altersklasseId: String,
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = null,
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true,
val createdAt: String,
val updatedAt: String
)
/**
* DTO for creating a new age class.
*/
@Serializable
data class CreateAltersklasseDto(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class CreateAltersklasseDto(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true
)
/**
* DTO for updating an existing age class.
*/
@Serializable
data class UpdateAltersklasseDto(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateAltersklasseDto(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = null,
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true
)
/**
* Configures the routing for age class endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/altersklassen") {
// GET /api/masterdata/altersklassen - Get all active age classes
get {
try {
val sparteFilterParam = call.request.queryParameters["sparte"]
val sparteFilter = sparteFilterParam?.let {
try {
SparteE.valueOf(it.uppercase())
} catch (_: Exception) {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse<List<AltersklasseDto>>("Invalid sparte parameter: $it")
)
}
}
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
)
}
}
val altersklassen = getAltersklasseUseCase.getAllActive(sparteFilter, geschlechtFilter)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/{id} - Get age class by ID
get("/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
val altersklasse = getAltersklasseUseCase.getById(altersklasseId)
if (altersklasse != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/code/{code} - Get age class by code
get("/code/{code}") {
try {
val altersklasseCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Age class code is required"))
val altersklasse = getAltersklasseUseCase.getByCode(altersklasseCode)
if (altersklasse != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>(e.message ?: "Invalid age class code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/search - Search age classes by name
get("/search") {
try {
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<AltersklasseDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val altersklassen = getAltersklasseUseCase.searchByName(searchTerm, limit)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to search age classes: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/age/{age} - Get age classes applicable for specific age
get("/age/{age}") {
try {
val age = call.parameters["age"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid age parameter"))
if (age < 0) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Age must be non-negative"))
}
val sparteFilterParam = call.request.queryParameters["sparte"]
val sparteFilter = sparteFilterParam?.let { SparteE.valueOf(it.uppercase()) }
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
)
}
}
val altersklassen = getAltersklasseUseCase.getApplicableForAge(age, sparteFilter, geschlechtFilter)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/sparte/{sparte} - Get age classes by sport type
get("/sparte/{sparte}") {
try {
val sparteParam = call.parameters["sparte"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Sport type is required"))
val sparte = try {
SparteE.valueOf(sparteParam.uppercase())
} catch (_: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid sport type: $sparteParam"))
}
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val altersklassen = getAltersklasseUseCase.getBySparte(sparte, activeOnly)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
}
}
// POST /api/masterdata/altersklassen - Create new age class
post {
try {
val createDto = call.receive<CreateAltersklasseDto>()
// Basic validation
if (createDto.altersklasseCode.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Age class code is required")
)
return@post
}
if (createDto.bezeichnung.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
)
return@post
}
val sparteFilter = createDto.sparteFilter?.let {
try {
SparteE.valueOf(it.uppercase())
} catch (_: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
)
}
}
val geschlechtFilter = createDto.geschlechtFilter?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
)
}
}
val oetoRegelReferenzId = createDto.oetoRegelReferenzId?.let {
try {
Uuid.parse(it)
} catch (_: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
)
}
}
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
altersklasseCode = createDto.altersklasseCode,
bezeichnung = createDto.bezeichnung,
minAlter = createDto.minAlter,
maxAlter = createDto.maxAlter,
stichtagRegelText = createDto.stichtagRegelText,
sparteFilter = sparteFilter,
geschlechtFilter = geschlechtFilter,
oetoRegelReferenzId = oetoRegelReferenzId,
istAktiv = createDto.istAktiv
)
val result = createAltersklasseUseCase.createAltersklasse(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.altersklasse!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to create age class: ${e.message}"))
}
}
// PUT /api/masterdata/altersklassen/{id} - Update existing age class
put("/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
val updateDto = call.receive<UpdateAltersklasseDto>()
// Basic validation
if (updateDto.altersklasseCode.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Age class code is required")
)
return@put
}
if (updateDto.bezeichnung.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
)
return@put
}
val sparteFilter = updateDto.sparteFilter?.let {
try {
SparteE.valueOf(it.uppercase())
} catch (_: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
)
}
}
val geschlechtFilter = updateDto.geschlechtFilter?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
)
}
}
val oetoRegelReferenzId = updateDto.oetoRegelReferenzId?.let {
try {
Uuid.parse(it)
} catch (_: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
)
}
}
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
altersklasseId = altersklasseId,
altersklasseCode = updateDto.altersklasseCode,
bezeichnung = updateDto.bezeichnung,
minAlter = updateDto.minAlter,
maxAlter = updateDto.maxAlter,
stichtagRegelText = updateDto.stichtagRegelText,
sparteFilter = sparteFilter,
geschlechtFilter = geschlechtFilter,
oetoRegelReferenzId = oetoRegelReferenzId,
istAktiv = updateDto.istAktiv
)
val result = createAltersklasseUseCase.updateAltersklasse(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.altersklasse!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to update age class: ${e.message}"))
}
}
// DELETE /api/masterdata/altersklassen/{id} - Delete age class
delete("/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid age class ID"))
val result = createAltersklasseUseCase.deleteAltersklasse(altersklasseId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Age class not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete age class: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/eligible/{id} - Check eligibility for age class
get("/eligible/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Invalid age class ID"))
val ageParam = call.request.queryParameters["age"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Age parameter is required"))
val geschlechtParam = call.request.queryParameters["geschlecht"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender parameter is required"))
if (geschlechtParam.length != 1 || (geschlechtParam != "M" && geschlechtParam != "W")) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender must be 'M' or 'W'"))
}
val isEligible = getAltersklasseUseCase.isEligible(altersklasseId, ageParam, geschlechtParam[0])
call.respond(HttpStatusCode.OK, ApiResponse.success(isEligible))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Boolean>("Failed to check eligibility: ${e.message}"))
}
}
fun Route.registerRoutes() {
route("/altersklassen") {
get {
val sparte = call.request.queryParameters["sparte"]?.let {
try {
SparteE.valueOf(it)
} catch (e: Exception) {
null
}
}
}
val geschlecht = call.request.queryParameters["geschlecht"]?.getOrNull(0)
/**
* Extension function to convert AltersklasseDefinition domain object to AltersklasseDto.
*/
private fun AltersklasseDefinition.toDto(): AltersklasseDto {
return AltersklasseDto(
altersklasseId = this.altersklasseId.toString(),
altersklasseCode = this.altersklasseCode,
bezeichnung = this.bezeichnung,
minAlter = this.minAlter,
maxAlter = this.maxAlter,
stichtagRegelText = this.stichtagRegelText,
sparteFilter = this.sparteFilter?.name,
geschlechtFilter = this.geschlechtFilter?.toString(),
oetoRegelReferenzId = this.oetoRegelReferenzId?.toString(),
istAktiv = this.istAktiv,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
val response = getAltersklasseUseCase.getAllActive(sparte, geschlecht)
val dtos = response.altersklassen.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
}
get("/{id}") {
val idStr = call.parameters["id"]
val id = idStr?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
)
val response = getAltersklasseUseCase.getById(id)
response.altersklasse?.let {
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Age class not found"))
}
post {
val dto = call.receive<CreateAltersklasseDto>()
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
altersklasseCode = dto.altersklasseCode,
bezeichnung = dto.bezeichnung,
minAlter = dto.minAlter,
maxAlter = dto.maxAlter,
stichtagRegelText = dto.stichtagRegelText,
sparteFilter = dto.sparteFilter?.let {
try {
SparteE.valueOf(it)
} catch (e: Exception) {
null
}
},
geschlechtFilter = dto.geschlechtFilter?.getOrNull(0),
oetoRegelReferenzId = dto.oetoRegelReferenzId?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
},
istAktiv = dto.istAktiv
)
val response = createAltersklasseUseCase.createAltersklasse(request)
val altersklasse = response.altersklasse
if (response.success && altersklasse != null) {
call.respond(HttpStatusCode.Created, ApiResponse.success(altersklasse.toDto()))
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
)
}
}
put("/{id}") {
val idStr = call.parameters["id"]
val id = idStr?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
}
?: return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
)
val dto = call.receive<UpdateAltersklasseDto>()
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
altersklasseId = id,
altersklasseCode = dto.altersklasseCode,
bezeichnung = dto.bezeichnung,
minAlter = dto.minAlter,
maxAlter = dto.maxAlter,
stichtagRegelText = dto.stichtagRegelText,
sparteFilter = dto.sparteFilter?.let {
try {
SparteE.valueOf(it)
} catch (e: Exception) {
null
}
},
geschlechtFilter = dto.geschlechtFilter?.getOrNull(0),
oetoRegelReferenzId = dto.oetoRegelReferenzId?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
},
istAktiv = dto.istAktiv
)
val response = createAltersklasseUseCase.updateAltersklasse(request)
val altersklasse = response.altersklasse
if (response.success && altersklasse != null) {
call.respond(ApiResponse.success(altersklasse.toDto()))
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("UPDATE_FAILED", response.errors.joinToString())
)
}
}
delete("/{id}") {
val idStr = call.parameters["id"]
val id = idStr?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
}
?: return@delete call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
)
val response = createAltersklasseUseCase.deleteAltersklasse(id)
if (response.success) {
call.respond(ApiResponse.success(Unit))
} else {
call.respond(
HttpStatusCode.NotFound,
ApiResponse.error<Unit>("DELETE_FAILED", response.errors.joinToString())
)
}
}
}
}
private fun AltersklasseDefinition.toDto() = AltersklasseDto(
altersklasseId = altersklasseId.toString(),
altersklasseCode = altersklasseCode,
bezeichnung = bezeichnung,
minAlter = minAlter,
maxAlter = maxAlter,
stichtagRegelText = stichtagRegelText,
sparteFilter = sparteFilter?.name,
geschlechtFilter = geschlechtFilter?.toString(),
oetoRegelReferenzId = oetoRegelReferenzId?.toString(),
istAktiv = istAktiv,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
)
}
@@ -5,7 +5,6 @@ import at.mocode.core.domain.model.ApiResponse
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.core.utils.validation.ApiValidationUtils
import kotlin.uuid.Uuid
import io.ktor.http.*
import io.ktor.server.request.*
@@ -14,19 +13,13 @@ import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* REST API controller for federal state management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* federal state functionality, following REST conventions and proper error handling.
* REST API controller for federal state (Bundesland) management.
*/
class BundeslandController(
private val getBundeslandUseCase: GetBundeslandUseCase,
private val createBundeslandUseCase: CreateBundeslandUseCase
) {
/**
* DTO for federal state API responses.
*/
@Serializable
data class BundeslandDto(
val bundeslandId: String,
@@ -42,9 +35,6 @@ class BundeslandController(
val updatedAt: String
)
/**
* DTO for creating a new federal state.
*/
@Serializable
data class CreateBundeslandDto(
val landId: String,
@@ -57,313 +47,97 @@ class BundeslandController(
val sortierReihenfolge: Int? = null
)
/**
* DTO for updating an existing federal state.
*/
@Serializable
data class UpdateBundeslandDto(
val landId: String,
val oepsCode: String? = null,
val iso3166_2_Code: String? = null,
val name: String,
val kuerzel: String? = null,
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Configures the routing for federal state endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/bundeslaender") {
// GET /api/masterdata/bundeslaender - Get all active federal states
fun Route.registerRoutes() {
route("/bundeslaender") {
get {
val landId = call.request.queryParameters["landId"]?.let {
try {
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = if (orderBySortierungParam != null) {
try {
orderBySortierungParam.toBoolean()
} catch (_: Exception) {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<BundeslandDto>>("Invalid orderBySortierung parameter. Must be true or false")
)
}
} else {
true
}
val bundeslaender = getBundeslandUseCase.getAllActive(orderBySortierung)
val bundeslandDtos = bundeslaender.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
Uuid.parse(it)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
null
}
}
val response = if (landId != null) {
getBundeslandUseCase.getByCountry(landId)
} else {
getBundeslandUseCase.getAllActive()
}
val dtos = response.bundeslaender.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
}
// GET /api/masterdata/bundeslaender/{id} - Get federal state by ID
get("/{id}") {
val idStr = call.parameters["id"]
val id = idStr?.let {
try {
val bundeslandId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
val bundesland = getBundeslandUseCase.getById(bundeslandId)
if (bundesland != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
}
Uuid.parse(it)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
null
}
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
)
val response = getBundeslandUseCase.getById(id)
response.bundesland?.let {
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(
HttpStatusCode.NotFound,
ApiResponse.error<Unit>("NOT_FOUND", "Federal state not found")
)
}
// GET /api/masterdata/bundeslaender/oeps/{code} - Get federal state by OEPS code
get("/oeps/{code}") {
try {
val oepsCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("OEPS code is required"))
val landIdParam = call.request.queryParameters["landId"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Country ID (landId) is required"))
val landId = try {
Uuid.parse(landIdParam)
} catch (_: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid country ID format"))
}
val bundesland = getBundeslandUseCase.getByOepsCode(oepsCode, landId)
if (bundesland != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid OEPS code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/iso/{code} - Get federal state by ISO 3166-2 code
get("/iso/{code}") {
try {
val isoCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("ISO 3166-2 code is required"))
val bundesland = getBundeslandUseCase.getByIso3166_2_Code(isoCode)
if (bundesland != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid ISO code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/country/{countryId} - Get federal states by country
get("/country/{countryId}") {
try {
val landId = call.parameters["countryId"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Invalid country ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
val bundeslaender = getBundeslandUseCase.getByCountry(landId, activeOnly, orderBySortierung)
val bundeslandDtos = bundeslaender.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/search - Search federal states by name
get("/search") {
try {
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<BundeslandDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val landIdParam = call.request.queryParameters["landId"]
val landId = landIdParam?.let { Uuid.parse(it) }
val bundeslaender = getBundeslandUseCase.searchByName(searchTerm, landId, limit)
val bundeslandDtos = bundeslaender.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to search federal states: ${e.message}"))
}
}
// POST /api/masterdata/bundeslaender - Create new federal state
post {
try {
val createDto = call.receive<CreateBundeslandDto>()
val dto = call.receive<CreateBundeslandDto>()
val landId = try {
Uuid.parse(dto.landId)
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_LAND_ID", "Invalid landId format")
)
}
// Basic validation
if (createDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Name is required")
)
return@post
}
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
landId = landId,
oepsCode = dto.oepsCode,
iso3166_2_Code = dto.iso3166_2_Code,
name = dto.name,
kuerzel = dto.kuerzel,
wappenUrl = dto.wappenUrl,
istAktiv = dto.istAktiv,
sortierReihenfolge = dto.sortierReihenfolge
)
try {
uuidFrom(createDto.landId)
} catch (_: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Invalid country ID format")
)
return@post
}
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
landId = uuidFrom(createDto.landId),
oepsCode = createDto.oepsCode,
iso3166_2_Code = createDto.iso3166_2_Code,
name = createDto.name,
kuerzel = createDto.kuerzel,
wappenUrl = createDto.wappenUrl,
istAktiv = createDto.istAktiv,
sortierReihenfolge = createDto.sortierReihenfolge
)
val result = createBundeslandUseCase.createBundesland(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.bundesland!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to create federal state: ${e.message}"))
}
val response = createBundeslandUseCase.createBundesland(request)
val bundesland = response.bundesland
if (response.success && bundesland != null) {
call.respond(HttpStatusCode.Created, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
)
}
}
// PUT /api/masterdata/bundeslaender/{id} - Update existing federal state
put("/{id}") {
try {
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
val updateDto = call.receive<UpdateBundeslandDto>()
// Basic validation
if (updateDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Name is required")
)
return@put
}
try {
uuidFrom(updateDto.landId)
} catch (_: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Invalid country ID format")
)
return@put
}
val request = CreateBundeslandUseCase.UpdateBundeslandRequest(
bundeslandId = bundeslandId,
landId = uuidFrom(updateDto.landId),
oepsCode = updateDto.oepsCode,
iso3166_2_Code = updateDto.iso3166_2_Code,
name = updateDto.name,
kuerzel = updateDto.kuerzel,
wappenUrl = updateDto.wappenUrl,
istAktiv = updateDto.istAktiv,
sortierReihenfolge = updateDto.sortierReihenfolge
)
val result = createBundeslandUseCase.updateBundesland(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.bundesland!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to update federal state: ${e.message}"))
}
}
// DELETE /api/masterdata/bundeslaender/{id} - Delete federal state
delete("/{id}") {
try {
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid federal state ID"))
val result = createBundeslandUseCase.deleteBundesland(bundeslandId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Federal state not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/count/{countryId} - Count active federal states by country
get("/count/{countryId}") {
try {
val landId = call.parameters["countryId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid country ID"))
val count = getBundeslandUseCase.countActiveByCountry(landId)
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count federal states: ${e.message}"))
}
}
}
}
}
/**
* Extension function to convert BundeslandDefinition domain object to BundeslandDto.
*/
private fun BundeslandDefinition.toDto(): BundeslandDto {
return BundeslandDto(
bundeslandId = this.bundeslandId.toString(),
landId = this.landId.toString(),
oepsCode = this.oepsCode,
iso3166_2_Code = this.iso3166_2_Code,
name = this.name,
kuerzel = this.kuerzel,
wappenUrl = this.wappenUrl,
istAktiv = this.istAktiv,
sortierReihenfolge = this.sortierReihenfolge,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
)
}
private fun BundeslandDefinition.toDto() = BundeslandDto(
bundeslandId = bundeslandId.toString(),
landId = landId.toString(),
oepsCode = oepsCode,
iso3166_2_Code = iso3166_2_Code,
name = name,
kuerzel = kuerzel,
wappenUrl = wappenUrl,
istAktiv = istAktiv,
sortierReihenfolge = sortierReihenfolge,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
)
}
@@ -5,7 +5,6 @@ import at.mocode.core.domain.model.ApiResponse
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
import at.mocode.masterdata.application.usecase.GetCountryUseCase
import at.mocode.masterdata.domain.model.LandDefinition
import at.mocode.core.utils.validation.ApiValidationUtils
import kotlin.uuid.Uuid
import io.ktor.http.*
import io.ktor.server.request.*
@@ -14,19 +13,13 @@ import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* REST API controller for country management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* country functionality, following REST conventions and proper error handling.
* REST API controller for country (Land) management.
*/
class CountryController(
private val getCountryUseCase: GetCountryUseCase,
private val createCountryUseCase: CreateCountryUseCase
) {
/**
* DTO for country API responses.
*/
@Serializable
data class CountryDto(
val landId: String,
@@ -44,9 +37,6 @@ class CountryController(
val updatedAt: String
)
/**
* DTO for creating a new country.
*/
@Serializable
data class CreateCountryDto(
val isoAlpha2Code: String,
@@ -61,294 +51,76 @@ class CountryController(
val sortierReihenfolge: Int? = null
)
/**
* DTO for updating an existing country.
*/
@Serializable
data class UpdateCountryDto(
val isoAlpha2Code: String,
val isoAlpha3Code: String,
val isoNumerischerCode: String? = null,
val nameDeutsch: String,
val nameEnglisch: String? = null,
val wappenUrl: String? = null,
val istEuMitglied: Boolean? = null,
val istEwrMitglied: Boolean? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Configures the routing for country endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/countries") {
// GET /api/masterdata/countries - Get all active countries
fun Route.registerRoutes() {
route("/countries") {
get {
try {
// Validate orderBySortierung parameter if provided
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = if (orderBySortierungParam != null) {
try {
orderBySortierungParam.toBoolean()
} catch (_: Exception) {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<CountryDto>>("Invalid orderBySortierung parameter. Must be true or false")
)
}
} else {
true
}
val countries = getCountryUseCase.getAllActive(orderBySortierung)
val countryDtos = countries.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve countries: ${e.message}"))
}
val response = getCountryUseCase.getAllActive()
val dtos = response.countries.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
}
// GET /api/masterdata/countries/{id} - Get country by ID
get("/{id}") {
val idStr = call.parameters["id"]
val id = idStr?.let {
try {
val countryId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
val country = getCountryUseCase.getById(countryId)
if (country != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
}
Uuid.parse(it)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
null
}
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
)
val response = getCountryUseCase.getById(id)
response.country?.let {
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Country not found"))
}
// GET /api/masterdata/countries/iso2/{code} - Get country by ISO Alpha-2 code
get("/iso2/{code}") {
try {
val isoCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
val country = getCountryUseCase.getByIsoAlpha2Code(isoCode)
if (country != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
}
}
// GET /api/masterdata/countries/iso3/{code} - Get country by ISO Alpha-3 code
get("/iso3/{code}") {
try {
val isoCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
val country = getCountryUseCase.getByIsoAlpha3Code(isoCode)
if (country != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
}
}
// GET /api/masterdata/countries/search - Search countries by name
get("/search") {
try {
// Validate query parameters
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<CountryDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val countries = getCountryUseCase.searchByName(searchTerm, limit)
val countryDtos = countries.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to search countries: ${e.message}"))
}
}
// GET /api/masterdata/countries/eu - Get EU member countries
get("/eu") {
try {
val countries = getCountryUseCase.getEuMembers()
val countryDtos = countries.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EU countries: ${e.message}"))
}
}
// GET /api/masterdata/countries/ewr - Get EWR member countries
get("/ewr") {
try {
val countries = getCountryUseCase.getEwrMembers()
val countryDtos = countries.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EWR countries: ${e.message}"))
}
}
// POST /api/masterdata/countries - Create new country
post {
try {
val createDto = call.receive<CreateCountryDto>()
val dto = call.receive<CreateCountryDto>()
val request = CreateCountryUseCase.CreateCountryRequest(
isoAlpha2Code = dto.isoAlpha2Code,
isoAlpha3Code = dto.isoAlpha3Code,
isoNumerischerCode = dto.isoNumerischerCode,
nameDeutsch = dto.nameDeutsch,
nameEnglisch = dto.nameEnglisch,
wappenUrl = dto.wappenUrl,
istEuMitglied = dto.istEuMitglied,
istEwrMitglied = dto.istEwrMitglied,
istAktiv = dto.istAktiv,
sortierReihenfolge = dto.sortierReihenfolge
)
// Validate input using shared validation utilities
val validationErrors = ApiValidationUtils.validateCountryRequest(
isoAlpha2Code = createDto.isoAlpha2Code,
isoAlpha3Code = createDto.isoAlpha3Code,
nameDeutsch = createDto.nameDeutsch,
nameEnglisch = createDto.nameEnglisch
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@post
}
val request = CreateCountryUseCase.CreateCountryRequest(
isoAlpha2Code = createDto.isoAlpha2Code,
isoAlpha3Code = createDto.isoAlpha3Code,
isoNumerischerCode = createDto.isoNumerischerCode,
nameDeutsch = createDto.nameDeutsch,
nameEnglisch = createDto.nameEnglisch,
wappenUrl = createDto.wappenUrl,
istEuMitglied = createDto.istEuMitglied,
istEwrMitglied = createDto.istEwrMitglied,
istAktiv = createDto.istAktiv,
sortierReihenfolge = createDto.sortierReihenfolge
)
val result = createCountryUseCase.createCountry(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.country!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to create country: ${e.message}"))
}
}
// PUT /api/masterdata/countries/{id} - Update existing country
put("/{id}") {
try {
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
val updateDto = call.receive<UpdateCountryDto>()
// Validate input using shared validation utilities
val validationErrors = ApiValidationUtils.validateCountryRequest(
isoAlpha2Code = updateDto.isoAlpha2Code,
isoAlpha3Code = updateDto.isoAlpha3Code,
nameDeutsch = updateDto.nameDeutsch,
nameEnglisch = updateDto.nameEnglisch
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@put
}
val request = CreateCountryUseCase.UpdateCountryRequest(
landId = countryId,
isoAlpha2Code = updateDto.isoAlpha2Code,
isoAlpha3Code = updateDto.isoAlpha3Code,
isoNumerischerCode = updateDto.isoNumerischerCode,
nameDeutsch = updateDto.nameDeutsch,
nameEnglisch = updateDto.nameEnglisch,
wappenUrl = updateDto.wappenUrl,
istEuMitglied = updateDto.istEuMitglied,
istEwrMitglied = updateDto.istEwrMitglied,
istAktiv = updateDto.istAktiv,
sortierReihenfolge = updateDto.sortierReihenfolge
)
val result = createCountryUseCase.updateCountry(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.country!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to update country: ${e.message}"))
}
}
// DELETE /api/masterdata/countries/{id} - Delete country
delete("/{id}") {
try {
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid country ID"))
val result = createCountryUseCase.deleteCountry(countryId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Country not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete country: ${e.message}"))
val response = createCountryUseCase.createCountry(request)
val country = response.country
if (response.success && country != null) {
call.respond(HttpStatusCode.Created, ApiResponse.success(country.toDto()))
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
)
}
}
}
}
/**
* Extension function to convert LandDefinition domain object to CountryDto.
*/
private fun LandDefinition.toDto(): CountryDto {
return CountryDto(
landId = this.landId.toString(),
isoAlpha2Code = this.isoAlpha2Code,
isoAlpha3Code = this.isoAlpha3Code,
isoNumerischerCode = this.isoNumerischerCode,
nameDeutsch = this.nameDeutsch,
nameEnglisch = this.nameEnglisch,
wappenUrl = this.wappenUrl,
istEuMitglied = this.istEuMitglied,
istEwrMitglied = this.istEwrMitglied,
istAktiv = this.istAktiv,
sortierReihenfolge = this.sortierReihenfolge,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
)
}
private fun LandDefinition.toDto() = CountryDto(
landId = landId.toString(),
isoAlpha2Code = isoAlpha2Code,
isoAlpha3Code = isoAlpha3Code,
isoNumerischerCode = isoNumerischerCode,
nameDeutsch = nameDeutsch,
nameEnglisch = nameEnglisch,
wappenUrl = wappenUrl,
istEuMitglied = istEuMitglied,
istEwrMitglied = istEwrMitglied,
istAktiv = istAktiv,
sortierReihenfolge = sortierReihenfolge,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
)
}
@@ -6,7 +6,6 @@ import at.mocode.core.domain.model.PlatzTypE
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
import at.mocode.masterdata.domain.model.Platz
import at.mocode.core.utils.validation.ApiValidationUtils
import kotlin.uuid.Uuid
import io.ktor.http.*
import io.ktor.server.request.*
@@ -15,22 +14,16 @@ import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* REST API controller for venue/arena management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* venue functionality, following REST conventions and proper error handling.
* REST API controller for venue/arena (Platz) management.
*/
class PlatzController(
private val getPlatzUseCase: GetPlatzUseCase,
private val createPlatzUseCase: CreatePlatzUseCase
) {
/**
* DTO for venue API responses.
*/
@Serializable
data class PlatzDto(
val id: String,
val platzId: String,
val turnierId: String,
val name: String,
val dimension: String? = null,
@@ -42,9 +35,6 @@ class PlatzController(
val updatedAt: String
)
/**
* DTO for creating a new venue.
*/
@Serializable
data class CreatePlatzDto(
val turnierId: String,
@@ -56,420 +46,95 @@ class PlatzController(
val sortierReihenfolge: Int? = null
)
/**
* DTO for updating an existing venue.
*/
@Serializable
data class UpdatePlatzDto(
val turnierId: String,
val name: String,
val dimension: String? = null,
val boden: String? = null,
val typ: String,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Configures the routing for venue endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/plaetze") {
// GET /api/masterdata/plaetze/{id} - Get venue by ID
get("/{id}") {
try {
val platzId = call.parameters["id"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
val platz = getPlatzUseCase.getById(platzId)
if (platz != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(platz.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<PlatzDto>("Venue not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to retrieve venue: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/tournament/{turnierId} - Get venues by tournament
get("/tournament/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"]?.let { Uuid.parse(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid tournament ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByTournament(turnierId, activeOnly, orderBySortierung)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/search - Search venues by name
get("/search") {
try {
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<PlatzDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val plaetze = getPlatzUseCase.searchByName(searchTerm, turnierId, limit)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to search venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/type/{typ} - Get venues by type
get("/type/{typ}") {
try {
val typParam = call.parameters["typ"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Venue type is required"))
val typ = try {
PlatzTypE.valueOf(typParam.uppercase())
} catch (_: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
}
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByType(typ, turnierId, activeOnly)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/ground/{boden} - Get venues by ground type
get("/ground/{boden}") {
try {
val boden = call.parameters["boden"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Ground type is required"))
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByGroundType(boden, turnierId, activeOnly)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid ground type"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/dimension/{dimension} - Get venues by dimensions
get("/dimension/{dimension}") {
try {
val dimension = call.parameters["dimension"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Dimension is required"))
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByDimensions(dimension, turnierId, activeOnly)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid dimension"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/suitable - Get venues suitable for discipline
get("/suitable") {
try {
val typParam = call.request.queryParameters["typ"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Required venue type parameter is missing"))
val requiredType = try {
PlatzTypE.valueOf(typParam.uppercase())
} catch (_: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
}
val requiredDimensions = call.request.queryParameters["dimension"]
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val plaetze = getPlatzUseCase.getSuitableForDiscipline(requiredType, requiredDimensions, turnierId)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve suitable venues: ${e.message}"))
}
}
// POST /api/masterdata/plaetze - Create new venue
post {
try {
val createDto = call.receive<CreatePlatzDto>()
// Basic validation
if (createDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Name is required")
)
return@post
}
val turnierId = try {
uuidFrom(createDto.turnierId)
} catch (_: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
)
}
val typ = try {
PlatzTypE.valueOf(createDto.typ.uppercase())
} catch (_: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid venue type: ${createDto.typ}")
)
}
val request = CreatePlatzUseCase.CreatePlatzRequest(
turnierId = turnierId,
name = createDto.name,
dimension = createDto.dimension,
boden = createDto.boden,
typ = typ,
istAktiv = createDto.istAktiv,
sortierReihenfolge = createDto.sortierReihenfolge
)
val result = createPlatzUseCase.createPlatz(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.platz!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to create venue: ${e.message}"))
}
}
// PUT /api/masterdata/plaetze/{id} - Update existing venue
put("/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
val updateDto = call.receive<UpdatePlatzDto>()
// Basic validation
if (updateDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Name is required")
)
return@put
}
val turnierId = try {
uuidFrom(updateDto.turnierId)
} catch (_: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
)
}
val typ = try {
PlatzTypE.valueOf(updateDto.typ.uppercase())
} catch (_: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid venue type: ${updateDto.typ}")
)
}
val request = CreatePlatzUseCase.UpdatePlatzRequest(
platzId = platzId,
turnierId = turnierId,
name = updateDto.name,
dimension = updateDto.dimension,
boden = updateDto.boden,
typ = typ,
istAktiv = updateDto.istAktiv,
sortierReihenfolge = updateDto.sortierReihenfolge
)
val result = createPlatzUseCase.updatePlatz(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.platz!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to update venue: ${e.message}"))
}
}
// DELETE /api/masterdata/plaetze/{id} - Delete venue
delete("/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid venue ID"))
val result = createPlatzUseCase.deletePlatz(platzId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Venue not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete venue: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/count/tournament/{turnierId} - Count venues by tournament
get("/count/tournament/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
val count = getPlatzUseCase.countActiveByTournament(turnierId)
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/count/type/{typ}/tournament/{turnierId} - Count venues by type and tournament
get("/count/type/{typ}/tournament/{turnierId}") {
try {
val typParam = call.parameters["typ"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Venue type is required"))
val typ = try {
PlatzTypE.valueOf(typParam.uppercase())
} catch (_: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid venue type: $typParam"))
}
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val count = getPlatzUseCase.countByTypeAndTournament(typ, turnierId, activeOnly)
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/grouped/tournament/{turnierId} - Get venues grouped by type
get("/grouped/tournament/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, List<PlatzDto>>>("Invalid tournament ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val groupedVenues = getPlatzUseCase.getGroupedByTypeForTournament(turnierId, activeOnly)
val groupedDtos = groupedVenues.mapKeys { it.key.name }.mapValues { entry ->
entry.value.map { it.toDto() }
}
call.respond(HttpStatusCode.OK, ApiResponse.success(groupedDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, List<PlatzDto>>>("Failed to retrieve grouped venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/validate/{id} - Validate venue suitability
get("/validate/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid venue ID"))
val requiredTypeParam = call.request.queryParameters["requiredType"]
val requiredType = requiredTypeParam?.let {
try {
PlatzTypE.valueOf(it.uppercase())
} catch (_: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid required type: $it"))
}
}
val requiredDimensions = call.request.queryParameters["requiredDimensions"]
val requiredGroundType = call.request.queryParameters["requiredGroundType"]
val (isValid, reasons) = getPlatzUseCase.validateVenueSuitability(platzId, requiredType, requiredDimensions, requiredGroundType)
val response = mapOf(
"isValid" to isValid,
"reasons" to reasons
)
call.respond(HttpStatusCode.OK, ApiResponse.success(response))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, Any>>("Failed to validate venue: ${e.message}"))
}
}
fun Route.registerRoutes() {
route("/plaetze") {
get {
val turnierId = call.request.queryParameters["turnierId"]?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
}
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("MISSING_TURNIER_ID", "Query parameter turnierId is required")
)
/**
* Extension function to convert Platz domain object to PlatzDto.
*/
private fun Platz.toDto(): PlatzDto {
return PlatzDto(
id = this.id.toString(),
turnierId = this.turnierId.toString(),
name = this.name,
dimension = this.dimension,
boden = this.boden,
typ = this.typ.name,
istAktiv = this.istAktiv,
sortierReihenfolge = this.sortierReihenfolge,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
)
val response = getPlatzUseCase.getByTournament(turnierId)
val dtos = response.plaetze.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
}
get("/{id}") {
val idStr = call.parameters["id"]
val id = idStr?.let {
try {
Uuid.parse(it)
} catch (e: Exception) {
null
}
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
)
val response = getPlatzUseCase.getById(id)
response.platz?.let {
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Venue not found"))
}
post {
val dto = call.receive<CreatePlatzDto>()
val turnierId = try {
Uuid.parse(dto.turnierId)
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_TURNIER_ID", "Invalid turnierId format")
)
}
val request = CreatePlatzUseCase.CreatePlatzRequest(
turnierId = turnierId,
name = dto.name,
dimension = dto.dimension,
boden = dto.boden,
typ = try {
PlatzTypE.valueOf(dto.typ)
} catch (e: Exception) {
PlatzTypE.SONSTIGE
},
istAktiv = dto.istAktiv,
sortierReihenfolge = dto.sortierReihenfolge
)
val response = createPlatzUseCase.createPlatz(request)
val platz = response.platz
if (response.success && platz != null) {
call.respond(HttpStatusCode.Created, ApiResponse.success(platz.toDto()))
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
)
}
}
}
}
private fun Platz.toDto() = PlatzDto(
platzId = id.toString(),
turnierId = turnierId.toString(),
name = name,
dimension = dimension,
boden = boden,
typ = typ.name,
istAktiv = istAktiv,
sortierReihenfolge = sortierReihenfolge,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
)
}
@@ -3,8 +3,9 @@ plugins {
}
dependencies {
implementation(projects.masterdata.masterdataDomain)
implementation(projects.backend.services.masterdata.masterdataDomain)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(libs.kotlinx.datetime)
testImplementation(projects.platform.platformTesting)
}
@@ -7,7 +7,7 @@ import at.mocode.masterdata.domain.repository.AltersklasseRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlin.time.Clock
/**
* Use case for creating and updating age class information.
@@ -209,7 +209,7 @@ class CreateAltersklasseUseCase(
}
/**
* Validates a create age class request.
* Validates a creation age class request.
*/
private fun validateCreateRequest(request: CreateAltersklasseRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
@@ -6,7 +6,7 @@ import at.mocode.masterdata.domain.repository.BundeslandRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlin.time.Clock
/**
* Use case for creating and updating federal state information.
@@ -6,7 +6,7 @@ import at.mocode.masterdata.domain.repository.LandRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlin.time.Clock
/**
* Use case for creating and updating country information.
@@ -7,7 +7,7 @@ import at.mocode.masterdata.domain.repository.PlatzRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlin.time.Clock
/**
* Use case for creating and updating venue/arena information.
@@ -22,10 +22,13 @@ class GetAltersklasseUseCase(
* @param altersklasseId The unique identifier of the age class
* @return The age class if found, null otherwise
*/
suspend fun getById(altersklasseId: Uuid): AltersklasseDefinition? {
return altersklasseRepository.findById(altersklasseId)
suspend fun getById(altersklasseId: Uuid): GetAltersklasseResponse {
val altersklasse = altersklasseRepository.findById(altersklasseId)
return GetAltersklasseResponse(altersklasse = altersklasse)
}
data class GetAltersklasseResponse(val altersklasse: AltersklasseDefinition?)
/**
* Retrieves an age class by its code.
*
@@ -57,13 +60,16 @@ class GetAltersklasseUseCase(
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of active age classes
*/
suspend fun getAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition> {
suspend fun getAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): GetAltersklassenResponse {
geschlechtFilter?.let { gender ->
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
}
return altersklasseRepository.findAllActive(sparteFilter, geschlechtFilter)
val altersklassen = altersklasseRepository.findAllActive(sparteFilter, geschlechtFilter)
return GetAltersklassenResponse(altersklassen = altersklassen)
}
data class GetAltersklassenResponse(val altersklassen: List<AltersklasseDefinition>)
/**
* Finds age classes applicable for a specific age.
*
@@ -21,10 +21,13 @@ class GetBundeslandUseCase(
* @param bundeslandId The unique identifier of the federal state
* @return The federal state if found, null otherwise
*/
suspend fun getById(bundeslandId: Uuid): BundeslandDefinition? {
return bundeslandRepository.findById(bundeslandId)
suspend fun getById(bundeslandId: Uuid): GetBundeslandResponse {
val bundesland = bundeslandRepository.findById(bundeslandId)
return GetBundeslandResponse(bundesland = bundesland)
}
data class GetBundeslandResponse(val bundesland: BundeslandDefinition?)
/**
* Retrieves a federal state by its OEPS code for a specific country.
*
@@ -56,22 +59,13 @@ class GetBundeslandUseCase(
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of federal states for the country
*/
suspend fun getByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<BundeslandDefinition> {
return bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
}
/**
* Searches for federal states by name (partial match).
*
* @param searchTerm The search term to match against federal state names
* @param landId Optional country ID to limit search
* @param limit Maximum number of results to return (default: 50)
* @return List of matching federal states
*/
suspend fun searchByName(searchTerm: String, landId: Uuid? = null, limit: Int = 50): List<BundeslandDefinition> {
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
require(limit > 0) { "Limit must be positive" }
return bundeslandRepository.findByName(searchTerm.trim(), landId, limit)
suspend fun getByCountry(
landId: Uuid,
activeOnly: Boolean = true,
orderBySortierung: Boolean = true
): GetBundeslaenderResponse {
val bundeslaender = bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
return GetBundeslaenderResponse(bundeslaender = bundeslaender)
}
/**
@@ -80,10 +74,13 @@ class GetBundeslandUseCase(
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of active federal states
*/
suspend fun getAllActive(orderBySortierung: Boolean = true): List<BundeslandDefinition> {
return bundeslandRepository.findAllActive(orderBySortierung)
suspend fun getAllActive(orderBySortierung: Boolean = true): GetBundeslaenderResponse {
val bundeslaender = bundeslandRepository.findAllActive(orderBySortierung)
return GetBundeslaenderResponse(bundeslaender = bundeslaender)
}
data class GetBundeslaenderResponse(val bundeslaender: List<BundeslandDefinition>)
/**
* Checks if a federal state with the given OEPS code exists for a country.
*
@@ -21,10 +21,13 @@ class GetCountryUseCase(
* @param countryId The unique identifier of the country
* @return The country if found, null otherwise
*/
suspend fun getById(countryId: Uuid): LandDefinition? {
return landRepository.findById(countryId)
suspend fun getById(countryId: Uuid): GetCountryResponse {
val country = landRepository.findById(countryId)
return GetCountryResponse(country = country)
}
data class GetCountryResponse(val country: LandDefinition?)
/**
* Retrieves a country by its ISO Alpha-2 code.
*
@@ -66,10 +69,13 @@ class GetCountryUseCase(
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of active countries
*/
suspend fun getAllActive(orderBySortierung: Boolean = true): List<LandDefinition> {
return landRepository.findAllActive(orderBySortierung)
suspend fun getAllActive(orderBySortierung: Boolean = true): GetCountriesResponse {
val countries = landRepository.findAllActive(orderBySortierung)
return GetCountriesResponse(countries = countries)
}
data class GetCountriesResponse(val countries: List<LandDefinition>)
/**
* Retrieves all EU member countries.
*
@@ -22,10 +22,13 @@ class GetPlatzUseCase(
* @param platzId The unique identifier of the venue
* @return The venue if found, null otherwise
*/
suspend fun getById(platzId: Uuid): Platz? {
return platzRepository.findById(platzId)
suspend fun getById(platzId: Uuid): GetPlatzResponse {
val platz = platzRepository.findById(platzId)
return GetPlatzResponse(platz = platz)
}
data class GetPlatzResponse(val platz: Platz?)
/**
* Retrieves all venues for a specific tournament.
*
@@ -34,10 +37,17 @@ class GetPlatzUseCase(
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of venues for the tournament
*/
suspend fun getByTournament(turnierId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<Platz> {
return platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
suspend fun getByTournament(
turnierId: Uuid,
activeOnly: Boolean = true,
orderBySortierung: Boolean = true
): GetPlaetzeResponse {
val plaetze = platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
return GetPlaetzeResponse(plaetze = plaetze)
}
data class GetPlaetzeResponse(val plaetze: List<Platz>)
/**
* Searches for venues by name (partial match).
*
@@ -1,6 +1,6 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
@@ -20,6 +20,10 @@ kotlin {
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmTest by getting {
dependencies {
implementation(projects.platform.platformTesting)
}
}
@@ -1,13 +1,12 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.model.SparteE // Optional, falls Altersklassen stark spartenspezifisch sind
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.model.SparteE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Definiert eine spezifische Altersklasse für Teilnehmer (Reiter, Fahrer, Voltigierer)
@@ -52,8 +51,8 @@ data class AltersklasseDefinition(
var istAktiv: Boolean = true,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant
)
@@ -1,12 +1,11 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Definiert ein Bundesland oder eine vergleichbare subnationale Verwaltungseinheit.
@@ -44,8 +43,8 @@ data class BundeslandDefinition(
var istAktiv: Boolean = true,
var sortierReihenfolge: Int? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant
)
@@ -0,0 +1,126 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.FunktionaerRolleE
import at.mocode.core.domain.model.RichterQualifikationE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für einen Funktionär im actor-context.
*
* Repräsentiert eine Person mit einer definierten Rolle bei Turnieren (Richter, TBA,
* Parcoursbauer, etc.). Die Qualifikation wird gegen `RICHT01.DAT` aus dem ZNS geprüft.
*
* Aggregate Root des `officials`-Bounded Context.
*
* @property funktionaerId Eindeutige interne ID (UUID).
* @property richterNummer ÖPS-Funktionärsnummer aus ZNS (RICHT01.dat), 6-stellig.
* @property vorname Vorname der Person.
* @property nachname Nachname der Person.
* @property geburtsdatum Geburtsdatum (optional, für Altersklassen-Prüfung).
* @property rollen Menge der Rollen, die diese Person ausüben darf (TBA, Richter, ...).
* @property richterQualifikation Qualifikationsstufe als Richter (GA, G1G3, International).
* @property qualifiziertFuerSparten Sparten, für die eine Richter-Qualifikation vorliegt.
* @property email E-Mail-Adresse für Kommunikation.
* @property telefon Telefonnummer.
* @property vereinsNummer Vereinsnummer des Heimvereins (Referenz auf DomVerein).
* @property istAktiv Ob der Funktionär aktuell aktiv/einsatzbereit ist.
* @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomFunktionaer(
@Serializable(with = UuidSerializer::class)
val funktionaerId: Uuid = Uuid.random(),
// Identifikation
val richterNummer: String? = null,
// Persönliche Daten
var vorname: String,
var nachname: String,
var geburtsdatum: LocalDate? = null,
// Qualifikation & Rollen
var rollen: Set<FunktionaerRolleE> = emptySet(),
var richterQualifikation: RichterQualifikationE? = null,
var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// Kontakt
var email: String? = null,
var telefon: String? = null,
// Vereinszugehörigkeit
var vereinsNummer: String? = null,
// Status & Verwaltung
var istAktiv: Boolean = true,
var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt den vollständigen Anzeigenamen zurück.
*/
fun getDisplayName(): String = "$vorname $nachname"
/**
* Gibt den Anzeigenamen mit Funktionärsnummer zurück (falls vorhanden).
*/
fun getDisplayNameWithNummer(): String =
richterNummer?.let { "${getDisplayName()} ($it)" } ?: getDisplayName()
/**
* Prüft, ob der Funktionär als Richter für eine bestimmte Sparte qualifiziert ist.
*/
fun istRichterFuerSparte(sparte: SparteE): Boolean =
rollen.contains(FunktionaerRolleE.RICHTER) && qualifiziertFuerSparten.contains(sparte)
/**
* Prüft, ob der Funktionär die Rolle TBA ausüben darf.
*/
fun istTba(): Boolean = rollen.contains(FunktionaerRolleE.TBA)
/**
* Validiert die Pflichtfelder für den Turniereinsatz.
* Gibt eine Liste von Warnungen zurück (kein harter Fehler Override-Event möglich).
*/
fun validateFuerTurniereinsatz(rolle: FunktionaerRolleE, sparte: SparteE? = null): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Funktionär ${getDisplayName()} ist nicht aktiv.")
}
if (!rollen.contains(rolle)) {
warnings.add("Funktionär ${getDisplayName()} hat keine Qualifikation für Rolle $rolle.")
}
if (rolle == FunktionaerRolleE.RICHTER && sparte != null && !istRichterFuerSparte(sparte)) {
warnings.add("Funktionär ${getDisplayName()} ist nicht als Richter für Sparte $sparte qualifiziert.")
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomFunktionaer = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,175 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate
import kotlinx.datetime.number
import kotlinx.datetime.todayIn
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* 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 Birthdate 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 = Uuid.random(),
// 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.MANUELL,
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::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 geburtsdatum?.let { birthDate ->
"$pferdeName (${birthDate.year})"
} ?: 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 = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
var age = today.year - birthDate.year
// Check if a birthday has occurred this year
if (today.month.number < birthDate.month.number ||
(today.month.number == birthDate.month.number && today.day < birthDate.day)
) {
age--
}
age
}
}
/**
* 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 an updated timestamp.
*/
fun withUpdatedTimestamp(): DomPferd {
return this.copy(updatedAt = Clock.System.now())
}
}
@@ -0,0 +1,136 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.LocalDateSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain model representing a rider (Reiter) in the actor-context.
*
* A rider is a specialization of a person with additional equestrian-specific
* attributes such as license, start card, and competition eligibility.
* Data is primarily sourced from the OEPS ZNS (LIZENZ01.DAT).
*
* Key rules (ÖTO):
* - A rider requires an active Startkarte (annual fee paid) to compete nationally.
* - LizenzKlasse determines which competition classes the rider may enter.
* - Satznummer (6-digit) is the primary key for ZNS data exchange.
* - Kopfnummer is NOT a unique identifier it can change.
*
* @property reiterId Unique internal identifier (UUID).
* @property personId Reference to the base DomPerson record (UUID).
* @property satznummer 6-digit ZNS primary key for data exchange. Primary key for ZNS.
* @property lizenzNummer OEPS license number (from ZNS LIZENZ01.DAT).
* @property lizenzKlasse License class determining competition eligibility (e.g. R1, RD2).
* @property lizenzSparten Disciplines for which the license is valid.
* @property startkartAktiv Whether the annual start card fee has been paid.
* @property startkartSaison Season year for which the start card is valid (e.g. 2026).
* @property feiId FEI international rider ID (optional).
* @property nation Nation code (e.g. AUT).
* @property geburtsdatum Date of birth (for age class validation).
* @property vereinsNummer Club number (OEPS).
* @property vereinsName Club name.
* @property istGastreiter Whether the rider is a guest rider (foreign nationality, not in Austrian club).
* @property istAktiv Whether the rider is currently active in the system.
* @property datenQuelle Source of the data.
* @property createdAt Timestamp when this record was created.
* @property updatedAt Timestamp when this record was last updated.
*/
@Serializable
data class DomReiter(
@Serializable(with = UuidSerializer::class)
val reiterId: Uuid = Uuid.random(),
// Reference to base person
@Serializable(with = UuidSerializer::class)
val personId: Uuid,
// ZNS Identification
val satznummer: String,
val lizenzNummer: String? = null,
// License & Eligibility
val lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
val lizenzSparten: List<SparteE> = emptyList(),
// Start Card (Startkarte) annual fee proof
val startkartAktiv: Boolean = false,
val startkartSaison: Int? = null,
// International
val feiId: String? = null,
val nation: String? = null,
// Personal Data (denormalized from DomPerson for performance)
val nachname: String,
val vorname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
// Club Affiliation
val vereinsNummer: String? = null,
val vereinsName: String? = null,
// Status
val istGastreiter: Boolean = false,
val istAktiv: Boolean = true,
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Returns the display name of the rider.
*/
fun getDisplayName(): String = "$vorname $nachname"
/**
* Checks if the rider is eligible to compete nationally.
* Requires an active start card (Startkarte).
*/
fun isStartberechtigt(): Boolean = istAktiv && startkartAktiv
/**
* Checks if the rider holds a license for the given discipline.
*/
fun hasLizenzForSparte(sparte: SparteE): Boolean =
lizenzKlasse == LizenzKlasseE.LIZENZFREI || lizenzSparten.contains(sparte)
/**
* Validates the rider for competition entry.
* Returns a list of warning messages (never hard errors TBA has final say).
*/
fun validateForNennung(sparte: SparteE): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Reiter ${getDisplayName()} ist nicht aktiv")
}
if (!startkartAktiv) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für Saison $startkartSaison")
}
if (!hasLizenzForSparte(sparte)) {
warnings.add("Reiter ${getDisplayName()} hat keine Lizenz für Sparte $sparte (Lizenzklasse: $lizenzKlasse)")
}
return warnings
}
/**
* Creates a copy of this rider with an updated timestamp.
*/
fun withUpdatedTimestamp(): DomReiter = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,121 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für einen Verein im actor-context.
*
* Repräsentiert einen OEPS-Mitgliedsverein, der als Veranstalter von Turnieren
* und als Heimverein von Reitern und Funktionären fungiert.
* Daten werden primär aus dem ZNS (VEREIN01.dat) importiert.
*
* Aggregate Root des `clubs`-Bounded Context.
*
* @property vereinId Eindeutige interne ID (UUID).
* @property vereinsNummer ÖPS-Vereinsnummer aus ZNS (VEREIN01.dat), 4-stellig. Primärschlüssel für ZNS-Datenaustausch.
* @property name Offizieller Vereinsname.
* @property kurzname Kurzbezeichnung des Vereins (optional).
* @property bundesland Bundesland, in dem der Verein ansässig ist.
* @property ort Ort / Stadt des Vereinssitzes.
* @property plz Postleitzahl.
* @property strasse Straße und Hausnummer.
* @property email Offizielle E-Mail-Adresse des Vereins.
* @property telefon Telefonnummer des Vereins.
* @property website Website-URL des Vereins.
* @property oepsRegionNummer Regionsnummer beim OEPS (Landesverband).
* @property istVeranstalter Ob der Verein als Veranstalter von Turnieren zugelassen ist.
* @property istAktiv Ob der Verein aktuell aktiv ist.
* @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomVerein(
@Serializable(with = UuidSerializer::class)
val vereinId: Uuid = Uuid.random(),
// Identifikation
val vereinsNummer: String,
// Stammdaten
var name: String,
var kurzname: String? = null,
// Adresse
var bundesland: String? = null,
var ort: String? = null,
var plz: String? = null,
var strasse: String? = null,
// Kontakt
var email: String? = null,
var telefon: String? = null,
var website: String? = null,
// OEPS-Verwaltung
var oepsRegionNummer: String? = null,
var istVeranstalter: Boolean = false,
// Status & Verwaltung
var istAktiv: Boolean = true,
var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt den Anzeigenamen zurück Kurzname bevorzugt, sonst vollständiger Name.
*/
fun getDisplayName(): String = kurzname ?: name
/**
* Gibt den vollständigen Anzeigenamen mit Vereinsnummer zurück.
*/
fun getDisplayNameWithNummer(): String = "${getDisplayName()} ($vereinsNummer)"
/**
* Prüft, ob vollständige Adressdaten vorhanden sind.
*/
fun hasCompleteAddress(): Boolean =
!ort.isNullOrBlank() && !plz.isNullOrBlank() && !strasse.isNullOrBlank()
/**
* Validiert den Verein für den Einsatz als Veranstalter.
* Gibt Warnungen zurück (kein harter Fehler Override-Event möglich).
*/
fun validateFuerVeranstaltung(): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Verein ${getDisplayName()} ist nicht aktiv.")
}
if (!istVeranstalter) {
warnings.add("Verein ${getDisplayName()} ist nicht als Veranstalter zugelassen.")
}
if (!hasCompleteAddress()) {
warnings.add("Verein ${getDisplayName()} hat keine vollständige Adresse hinterlegt.")
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomVerein = this.copy(updatedAt = Clock.System.now())
}
@@ -1,12 +1,11 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Definiert ein Land/eine Nation mit seinen offiziellen Codes und Bezeichnungen.
@@ -44,8 +43,8 @@ data class LandDefinition(
var istAktiv: Boolean = true,
var sortierReihenfolge: Int? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant
)
@@ -2,12 +2,11 @@
package at.mocode.masterdata.domain.model
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Definiert einen Turnierplatz oder eine Wettkampfstätte.
@@ -41,8 +40,8 @@ data class Platz(
var istAktiv: Boolean = true,
var sortierReihenfolge: Int? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant
)
@@ -0,0 +1,144 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import kotlin.uuid.Uuid
/**
* Repository interface for AltersklasseDefinition (Age Class) domain operations.
*
* This interface defines the contract for age class 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 AltersklasseRepository {
/**
* Finds an age class by its unique ID.
*
* @param id The unique identifier of the age class
* @return The age class if found, null otherwise
*/
suspend fun findById(id: Uuid): AltersklasseDefinition?
/**
* Finds an age class by its code.
*
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
* @return The age class if found, null otherwise
*/
suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition?
/**
* Finds age classes by name (partial match).
*
* @param searchTerm The search term to match against age class names
* @param limit Maximum number of results to return
* @return List of matching age classes
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition>
/**
* Finds all active age classes.
*
* @param sparteFilter Optional filter by sport type
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of active age classes
*/
suspend fun findAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
/**
* Finds age classes applicable for a specific age.
*
* @param age The age to check
* @param sparteFilter Optional filter by sport type
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of applicable age classes
*/
suspend fun findApplicableForAge(
age: Int,
sparteFilter: SparteE? = null,
geschlechtFilter: Char? = null
): List<AltersklasseDefinition>
/**
* Finds age classes by sport type.
*
* @param sparte The sport type
* @param activeOnly Whether to return only active age classes
* @return List of age classes for the sport type
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition>
/**
* Finds age classes by gender filter.
*
* @param geschlecht The gender ('M', 'W')
* @param activeOnly Whether to return only active age classes
* @return List of age classes for the gender
*/
suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition>
/**
* Finds age classes by age range.
*
* @param minAge Minimum age (inclusive)
* @param maxAge Maximum age (inclusive)
* @param activeOnly Whether to return only active age classes
* @return List of age classes within the age range
*/
suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition>
/**
* Finds age classes by OETO rule reference.
*
* @param oetoRegelReferenzId The OETO rule reference ID
* @return List of age classes linked to the rule
*/
suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition>
/**
* Saves an age class (create or update).
*
* @param altersklasse The age class to save
* @return The saved age class with updated timestamps
*/
suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition
/**
* Deletes an age class by ID.
*
* @param id The unique identifier of the age class to delete
* @return true if the age class was deleted, false if not found
*/
suspend fun delete(id: Uuid): Boolean
/**
* Checks if an age class with the given code exists.
*
* @param altersklasseCode The age class code to check
* @return true if an age class with this code exists, false otherwise
*/
suspend fun existsByCode(altersklasseCode: String): Boolean
/**
* Counts the total number of active age classes.
*
* @param sparteFilter Optional filter by sport type
* @return The total count of active age classes
*/
suspend fun countActive(sparteFilter: SparteE? = null): Long
/**
* Validates if a person with given age and gender can participate in an age class.
*
* @param altersklasseId The age class ID
* @param age The person's age
* @param geschlecht The person's gender ('M', 'W')
* @return true if the person can participate, false otherwise
*/
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean
}
@@ -0,0 +1,93 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.core.domain.model.FunktionaerRolleE
import at.mocode.core.domain.model.RichterQualifikationE
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.DomFunktionaer
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomFunktionaer (Funktionär) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank, etc.).
*/
interface FunktionaerRepository {
/**
* Sucht einen Funktionär anhand seiner eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomFunktionaer?
/**
* Sucht einen Funktionär anhand seiner Richternummer.
*/
suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer?
/**
* Sucht Funktionäre anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomFunktionaer>
/**
* Sucht alle Funktionäre mit einer bestimmten Rolle.
*/
suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Richter mit einer bestimmten Qualifikation.
*/
suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean = true
): List<DomFunktionaer>
/**
* Sucht alle Funktionäre, die für eine bestimmte Sparte qualifiziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Funktionäre eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Gibt alle aktiven Funktionäre zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomFunktionaer>
/**
* Gibt alle Funktionäre zurück (paginiert).
*/
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomFunktionaer>
/**
* Speichert einen Funktionär (Insert oder Update).
*/
suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer
/**
* Löscht einen Funktionär anhand seiner ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle aktiven Funktionäre.
*/
suspend fun countActive(): Long
/**
* Zählt alle Richter (Rolle = RICHTER) mit einer bestimmten Qualifikation.
*/
suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean = true): Long
/**
* Prüft ob ein Funktionär mit der gegebenen Richternummer bereits existiert.
*/
suspend fun existsByRichterNummer(richterNummer: String): Boolean
}
@@ -0,0 +1,248 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.core.domain.model.PferdeGeschlechtE
import kotlin.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
/**
* Counts horses with OEPS registration.
*
* @param activeOnly Whether to count only active horses
* @return The count of OEPS registered horses
*/
suspend fun countOepsRegistered(activeOnly: Boolean = true): Long
/**
* Counts horses with FEI registration.
*
* @param activeOnly Whether to count only active horses
* @return The count of FEI registered horses
*/
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long
}
@@ -0,0 +1,89 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.DomReiter
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomReiter (Reiter) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank, etc.).
*/
interface ReiterRepository {
/**
* Sucht einen Reiter anhand seiner eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomReiter?
/**
* Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer).
*/
suspend fun findBySatznummer(satznummer: String): DomReiter?
/**
* Sucht einen Reiter anhand seiner FEI-ID.
*/
suspend fun findByFeiId(feiId: String): DomReiter?
/**
* Sucht Reiter anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomReiter>
/**
* Sucht alle Reiter eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter mit einer bestimmten Lizenzklasse.
*/
suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter, die für eine bestimmte Sparte lizenziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Gastreiter.
*/
suspend fun findGastreiter(activeOnly: Boolean = true): List<DomReiter>
/**
* Gibt alle aktiven Reiter zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomReiter>
/**
* Gibt alle Reiter zurück (paginiert).
*/
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomReiter>
/**
* Speichert einen Reiter (Insert oder Update).
*/
suspend fun save(reiter: DomReiter): DomReiter
/**
* Löscht einen Reiter anhand seiner ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle aktiven Reiter.
*/
suspend fun countActive(): Long
/**
* Prüft ob ein Reiter mit der gegebenen Satznummer bereits existiert.
*/
suspend fun existsBySatznummer(satznummer: String): Boolean
}
@@ -0,0 +1,72 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.masterdata.domain.model.DomVerein
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomVerein (Verein) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank etc.).
*/
interface VereinRepository {
/**
* Sucht einen Verein anhand seiner eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomVerein?
/**
* Sucht einen Verein anhand seiner OEPS-Vereinsnummer.
*/
suspend fun findByVereinsNummer(vereinsNummer: String): DomVerein?
/**
* Sucht Vereine anhand des Namens (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomVerein>
/**
* Sucht alle Vereine eines Bundeslandes.
*/
suspend fun findByBundesland(bundesland: String, activeOnly: Boolean = true): List<DomVerein>
/**
* Sucht alle Vereine, die als Veranstalter markiert sind.
*/
suspend fun findVeranstalter(activeOnly: Boolean = true): List<DomVerein>
/**
* Gibt alle aktiven Vereine zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomVerein>
/**
* Gibt alle Vereine zurück (paginiert).
*/
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<DomVerein>
/**
* Speichert einen Verein (Insert oder Update).
*/
suspend fun save(verein: DomVerein): DomVerein
/**
* Löscht einen Verein anhand seiner ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle aktiven Vereine.
*/
suspend fun countActive(): Long
/**
* Prüft ob ein Verein mit der gegebenen Vereinsnummer bereits existiert.
*/
suspend fun existsByVereinsNummer(vereinsNummer: String): Boolean
}
@@ -1,139 +0,0 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import kotlin.uuid.Uuid
/**
* Repository interface for AltersklasseDefinition (Age Class) domain operations.
*
* This interface defines the contract for age class 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 AltersklasseRepository {
/**
* Finds an age class by its unique ID.
*
* @param id The unique identifier of the age class
* @return The age class if found, null otherwise
*/
suspend fun findById(id: Uuid): AltersklasseDefinition?
/**
* Finds an age class by its code.
*
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
* @return The age class if found, null otherwise
*/
suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition?
/**
* Finds age classes by name (partial match).
*
* @param searchTerm The search term to match against age class names
* @param limit Maximum number of results to return
* @return List of matching age classes
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition>
/**
* Finds all active age classes.
*
* @param sparteFilter Optional filter by sport type
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of active age classes
*/
suspend fun findAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
/**
* Finds age classes applicable for a specific age.
*
* @param age The age to check
* @param sparteFilter Optional filter by sport type
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of applicable age classes
*/
suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
/**
* Finds age classes by sport type.
*
* @param sparte The sport type
* @param activeOnly Whether to return only active age classes
* @return List of age classes for the sport type
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition>
/**
* Finds age classes by gender filter.
*
* @param geschlecht The gender ('M', 'W')
* @param activeOnly Whether to return only active age classes
* @return List of age classes for the gender
*/
suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition>
/**
* Finds age classes by age range.
*
* @param minAge Minimum age (inclusive)
* @param maxAge Maximum age (inclusive)
* @param activeOnly Whether to return only active age classes
* @return List of age classes within the age range
*/
suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition>
/**
* Finds age classes by OETO rule reference.
*
* @param oetoRegelReferenzId The OETO rule reference ID
* @return List of age classes linked to the rule
*/
suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition>
/**
* Saves an age class (create or update).
*
* @param altersklasse The age class to save
* @return The saved age class with updated timestamps
*/
suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition
/**
* Deletes an age class by ID.
*
* @param id The unique identifier of the age class to delete
* @return true if the age class was deleted, false if not found
*/
suspend fun delete(id: Uuid): Boolean
/**
* Checks if an age class with the given code exists.
*
* @param altersklasseCode The age class code to check
* @return true if an age class with this code exists, false otherwise
*/
suspend fun existsByCode(altersklasseCode: String): Boolean
/**
* Counts the total number of active age classes.
*
* @param sparteFilter Optional filter by sport type
* @return The total count of active age classes
*/
suspend fun countActive(sparteFilter: SparteE? = null): Long
/**
* Validates if a person with given age and gender can participate in an age class.
*
* @param altersklasseId The age class ID
* @param age The person's age
* @param geschlecht The person's gender ('M', 'W')
* @return true if the person can participate, false otherwise
*/
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean
}
@@ -1,27 +1,26 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
alias(libs.plugins.spring.boot)
// Dependency Management für konsistente Spring-Versionen
kotlin("jvm")
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinSpring)
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.masterdata.masterdataDomain)
implementation(projects.masterdata.masterdataApplication)
implementation(projects.backend.services.masterdata.masterdataDomain)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.infrastructure.cache.cacheApi)
implementation(projects.infrastructure.eventStore.eventStoreApi)
implementation(projects.infrastructure.messaging.messagingClient)
implementation(projects.backend.infrastructure.cache.cacheApi)
implementation(projects.backend.infrastructure.eventStore.eventStoreApi)
implementation(projects.backend.infrastructure.messaging.messagingClient)
// Exposed
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlin.datetime)
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.postgresql:postgresql")
testImplementation(projects.platform.platformTesting)
@@ -1,240 +1,220 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.masterdata.domain.repository.AltersklasseRepository
import at.mocode.core.utils.database.DatabaseFactory
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des AltersklasseRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der AltersklasseDefinition Domain-Entität und der AltersklasseTable.
*/
class AltersklasseRepositoryImpl : AltersklasseRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
return AltersklasseDefinition(
altersklasseId = row[AltersklasseTable.id],
altersklasseCode = row[AltersklasseTable.altersklasseCode],
bezeichnung = row[AltersklasseTable.bezeichnung],
minAlter = row[AltersklasseTable.minAlter],
maxAlter = row[AltersklasseTable.maxAlter],
stichtagRegelText = row[AltersklasseTable.stichtagRegelText],
sparteFilter = row[AltersklasseTable.sparteFilter]?.let { SparteE.valueOf(it) },
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
istAktiv = row[AltersklasseTable.istAktiv],
createdAt = row[AltersklasseTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[AltersklasseTable.updatedAt].toInstant(TimeZone.UTC)
)
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
return AltersklasseDefinition(
altersklasseId = row[AltersklasseTable.id],
altersklasseCode = row[AltersklasseTable.altersklasseCode],
bezeichnung = row[AltersklasseTable.bezeichnung],
minAlter = row[AltersklasseTable.minAlter],
maxAlter = row[AltersklasseTable.maxAlter],
stichtagRegelText = row[AltersklasseTable.stichtagRegelText],
sparteFilter = row[AltersklasseTable.sparteFilter]?.let { SparteE.valueOf(it) },
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
istAktiv = row[AltersklasseTable.istAktiv],
createdAt = row[AltersklasseTable.createdAt],
updatedAt = row[AltersklasseTable.updatedAt]
)
}
override suspend fun findById(id: Uuid): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.id eq id }
.map(::rowToAltersklasseDefinition)
.singleOrNull()
}
override suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
.map(::rowToAltersklasseDefinition)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> =
DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
.limit(limit)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findById(id: Uuid): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.id eq id }
.map(::rowToAltersklasseDefinition)
.singleOrNull()
}
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> =
DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
override suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
.map(::rowToAltersklasseDefinition)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
.limit(limit)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
geschlechtFilter?.let { geschlecht ->
query.andWhere {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
// Age range filter
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
geschlechtFilter?.let { geschlecht ->
query.andWhere {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
}
geschlechtFilter?.let { geschlecht ->
query.andWhere {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
override suspend fun findApplicableForAge(
age: Int,
sparteFilter: SparteE?,
geschlechtFilter: Char?
): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
query.andWhere {
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
}
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll()
minAge?.let { min ->
query.andWhere {
(AltersklasseTable.maxAlter.isNull()) or (AltersklasseTable.maxAlter greaterEq min)
}
}
maxAge?.let { max ->
query.andWhere {
(AltersklasseTable.minAlter.isNull()) or (AltersklasseTable.minAlter lessEq max)
}
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
geschlechtFilter?.let { geschlecht ->
query.andWhere {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
}
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> =
DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingAltersklasse = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.singleOrNull()
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> =
DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
if (existingAltersklasse == null) {
// Insert a new age class
AltersklasseTable.insert { stmt ->
stmt[id] = altersklasse.altersklasseId
stmt[altersklasseCode] = altersklasse.altersklasseCode
stmt[bezeichnung] = altersklasse.bezeichnung
stmt[minAlter] = altersklasse.minAlter
stmt[maxAlter] = altersklasse.maxAlter
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
stmt[sparteFilter] = altersklasse.sparteFilter?.name
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
stmt[istAktiv] = altersklasse.istAktiv
stmt[createdAt] = altersklasse.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing age class
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) { stmt ->
stmt[altersklasseCode] = altersklasse.altersklasseCode
stmt[bezeichnung] = altersklasse.bezeichnung
stmt[minAlter] = altersklasse.minAlter
stmt[maxAlter] = altersklasse.maxAlter
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
stmt[sparteFilter] = altersklasse.sparteFilter?.name
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
stmt[istAktiv] = altersklasse.istAktiv
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
altersklasse.copy(updatedAt = now)
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.deleteWhere { AltersklasseTable.id eq id } > 0
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> =
DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll()
if (minAge != null) {
query.andWhere { AltersklasseTable.minAlter greaterEq minAge }
}
if (maxAge != null) {
query.andWhere { AltersklasseTable.maxAlter lessEq maxAge }
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
.count() > 0
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> =
DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
.map(::rowToAltersklasseDefinition)
}
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
val exists = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.any()
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
query.count()
if (exists) {
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) {
it[altersklasseCode] = altersklasse.altersklasseCode
it[bezeichnung] = altersklasse.bezeichnung
it[minAlter] = altersklasse.minAlter
it[maxAlter] = altersklasse.maxAlter
it[stichtagRegelText] = altersklasse.stichtagRegelText
it[sparteFilter] = altersklasse.sparteFilter?.name
it[geschlechtFilter] = altersklasse.geschlechtFilter
it[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
it[istAktiv] = altersklasse.istAktiv
it[updatedAt] = altersklasse.updatedAt
}
altersklasse
} else {
AltersklasseTable.insert {
it[id] = altersklasse.altersklasseId
it[altersklasseCode] = altersklasse.altersklasseCode
it[bezeichnung] = altersklasse.bezeichnung
it[minAlter] = altersklasse.minAlter
it[maxAlter] = altersklasse.maxAlter
it[stichtagRegelText] = altersklasse.stichtagRegelText
it[sparteFilter] = altersklasse.sparteFilter?.name
it[geschlechtFilter] = altersklasse.geschlechtFilter
it[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
it[istAktiv] = altersklasse.istAktiv
it[createdAt] = altersklasse.createdAt
it[updatedAt] = altersklasse.updatedAt
}
altersklasse
}
}
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
val altersklasse = AltersklasseTable.selectAll().where {
(AltersklasseTable.id eq altersklasseId) and (AltersklasseTable.istAktiv eq true)
}.singleOrNull()
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.deleteWhere { AltersklasseTable.id eq id } > 0
}
if (altersklasse == null) return@dbQuery false
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }.any()
}
// Check age eligibility
val minAlter = altersklasse[AltersklasseTable.minAlter]
val maxAlter = altersklasse[AltersklasseTable.maxAlter]
val ageEligible = (minAlter == null || age >= minAlter) && (maxAlter == null || age <= maxAlter)
// Check gender eligibility
val geschlechtFilter = altersklasse[AltersklasseTable.geschlechtFilter]
val genderEligible = geschlechtFilter == null || geschlechtFilter == geschlecht
ageEligible && genderEligible
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
sparteFilter?.let { sparte ->
query.andWhere { (AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull()) }
}
query.count()
}
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasseId }
.map {
val min = it[AltersklasseTable.minAlter]
val max = it[AltersklasseTable.maxAlter]
val g = it[AltersklasseTable.geschlechtFilter]
val ageOk = (min == null || age >= min) && (max == null || age <= max)
val geschlechtOk = (g == null || g == geschlecht)
ageOk && geschlechtOk
}.singleOrNull() ?: false
}
}
@@ -1,37 +1,38 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinitionen).
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinition).
*
* Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer
* entsprechend der AltersklasseDefinition Domain-Entität.
*/
object AltersklasseTable : Table("altersklasse") {
val id = javaUUID("id").autoGenerate()
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 200)
val minAlter = integer("min_alter").nullable()
val maxAlter = integer("max_alter").nullable()
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
val geschlechtFilter = char("geschlecht_filter").nullable()
val oetoRegelReferenzId = javaUUID("oeto_regel_referenz_id").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
val id = uuid("id")
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 200)
val minAlter = integer("min_alter").nullable()
val maxAlter = integer("max_alter").nullable()
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
val geschlechtFilter = char("geschlecht_filter").nullable()
val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
override val primaryKey = PrimaryKey(id)
init {
// Index for performance on common queries
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
index(customIndexName = "idx_altersklasse_alter", columns = arrayOf(minAlter, maxAlter))
}
init {
// Index for performance on common queries
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
index(customIndexName = "idx_altersklasse_alter", columns = arrayOf(minAlter, maxAlter))
}
}
@@ -1,158 +1,136 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.masterdata.domain.repository.BundeslandRepository
import at.mocode.core.utils.database.DatabaseFactory
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des BundeslandRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der BundeslandDefinition Domain-Entität und der BundeslandTable.
*/
class BundeslandRepositoryImpl : BundeslandRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
return BundeslandDefinition(
bundeslandId = row[BundeslandTable.id],
landId = row[BundeslandTable.landId],
oepsCode = row[BundeslandTable.oepsCode],
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
name = row[BundeslandTable.name],
kuerzel = row[BundeslandTable.kuerzel],
wappenUrl = row[BundeslandTable.wappenUrl],
istAktiv = row[BundeslandTable.istAktiv],
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
createdAt = row[BundeslandTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[BundeslandTable.updatedAt].toInstant(TimeZone.UTC)
)
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
return BundeslandDefinition(
bundeslandId = row[BundeslandTable.id],
landId = row[BundeslandTable.landId],
oepsCode = row[BundeslandTable.oepsCode],
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
name = row[BundeslandTable.name],
kuerzel = row[BundeslandTable.kuerzel],
wappenUrl = row[BundeslandTable.wappenUrl],
istAktiv = row[BundeslandTable.istAktiv],
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
createdAt = row[BundeslandTable.createdAt],
updatedAt = row[BundeslandTable.updatedAt]
)
}
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByCountry(
landId: Uuid,
activeOnly: Boolean,
orderBySortierung: Boolean
): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
if (activeOnly) {
query.andWhere { BundeslandTable.istAktiv eq true }
}
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> =
DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
landId?.let { query.andWhere { BundeslandTable.landId eq it } }
query.limit(limit).map(::rowToBundeslandDefinition)
}
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
.map(::rowToBundeslandDefinition)
.singleOrNull()
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where {
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
}
.map(::rowToBundeslandDefinition)
.singleOrNull()
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
val exists = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.any()
if (exists) {
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) {
it[landId] = bundesland.landId
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
it[kuerzel] = bundesland.kuerzel
it[wappenUrl] = bundesland.wappenUrl
it[istAktiv] = bundesland.istAktiv
it[sortierReihenfolge] = bundesland.sortierReihenfolge
it[updatedAt] = bundesland.updatedAt
}
bundesland
} else {
BundeslandTable.insert {
it[id] = bundesland.bundeslandId
it[landId] = bundesland.landId
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
it[kuerzel] = bundesland.kuerzel
it[wappenUrl] = bundesland.wappenUrl
it[istAktiv] = bundesland.istAktiv
it[sortierReihenfolge] = bundesland.sortierReihenfolge
it[createdAt] = bundesland.createdAt
it[updatedAt] = bundesland.updatedAt
}
bundesland
}
}
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
}
override suspend fun findByCountry(landId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
.any()
}
if (activeOnly) {
query.andWhere { BundeslandTable.istAktiv eq true }
}
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }.any()
}
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
landId?.let {
query.andWhere { BundeslandTable.landId eq it }
}
query.limit(limit).map(::rowToBundeslandDefinition)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingBundesland = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.singleOrNull()
if (existingBundesland == null) {
// Insert a new federal state
BundeslandTable.insert { stmt ->
stmt[id] = bundesland.bundeslandId
stmt[landId] = bundesland.landId
stmt[oepsCode] = bundesland.oepsCode
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
stmt[name] = bundesland.name
stmt[kuerzel] = bundesland.kuerzel
stmt[wappenUrl] = bundesland.wappenUrl
stmt[istAktiv] = bundesland.istAktiv
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
stmt[createdAt] = bundesland.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing federal state
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) { stmt ->
stmt[landId] = bundesland.landId
stmt[oepsCode] = bundesland.oepsCode
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
stmt[name] = bundesland.name
stmt[kuerzel] = bundesland.kuerzel
stmt[wappenUrl] = bundesland.wappenUrl
stmt[istAktiv] = bundesland.istAktiv
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
bundesland.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
}
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where {
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
}.count() > 0
}
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
.count() > 0
}
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where {
(BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true)
}.count()
}
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { (BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true) }
.count()
}
}
@@ -1,35 +1,31 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen).
*
* Diese Tabelle speichert alle Informationen zu Bundesländern und subnationalen
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
* Exposed-Tabellendefinition für die Bundesland-Entität.
*/
object BundeslandTable : Table("bundesland") {
val id = javaUUID("id").autoGenerate()
val landId = javaUUID("land_id").references(LandTable.id)
val oepsCode = varchar("oeps_code", 10).nullable()
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
val name = varchar("name", 100)
val kuerzel = varchar("kuerzel", 10).nullable()
val wappenUrl = varchar("wappen_url", 500).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
val id = uuid("bundesland_id")
val landId = uuid("land_id")
val oepsCode = varchar("oeps_code", 10).nullable()
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
val name = varchar("name", 100)
val kuerzel = varchar("kuerzel", 10).nullable()
val wappenUrl = varchar("wappen_url", 255).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
override val primaryKey = PrimaryKey(id)
init {
// Unique constraint for OEPS code per country
uniqueIndex("uk_bundesland_oeps_land", oepsCode, landId)
// Unique constraint for ISO 3166-2 code globally
uniqueIndex("uk_bundesland_iso3166_2", iso3166_2_Code)
}
init {
uniqueIndex("idx_bundesland_oeps", oepsCode, landId)
uniqueIndex("idx_bundesland_iso", iso3166_2_Code)
}
}
@@ -0,0 +1,157 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.FunktionaerRolleE
import at.mocode.core.domain.model.RichterQualifikationE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomFunktionaer
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Funktionaer-Repositorys.
*/
class ExposedFunktionaerRepository : FunktionaerRepository {
private fun rowToDomFunktionaer(row: ResultRow): DomFunktionaer {
return DomFunktionaer(
funktionaerId = row[FunktionaerTable.id],
richterNummer = row[FunktionaerTable.richterNummer],
vorname = row[FunktionaerTable.vorname],
nachname = row[FunktionaerTable.nachname],
geburtsdatum = row[FunktionaerTable.geburtsdatum],
email = row[FunktionaerTable.email],
telefon = row[FunktionaerTable.telefon],
vereinsNummer = row[FunktionaerTable.vereinsNummer],
istAktiv = row[FunktionaerTable.istAktiv],
bemerkungen = row[FunktionaerTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]),
createdAt = row[FunktionaerTable.createdAt],
updatedAt = row[FunktionaerTable.updatedAt]
)
}
override suspend fun findById(id: Uuid): DomFunktionaer? = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
.map(::rowToDomFunktionaer)
.singleOrNull()
}
override suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer? = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }
.map(::rowToDomFunktionaer)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
FunktionaerTable.selectAll()
.where { (FunktionaerTable.nachname like pattern) or (FunktionaerTable.vorname like pattern) }
.limit(limit)
.map(::rowToDomFunktionaer)
}
override suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
// Rolle wird aktuell nicht in FunktionaerTable gespeichert.
// Falls benötigt, muss die Tabelle erweitert werden.
emptyList()
}
override suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean
): List<DomFunktionaer> = DatabaseFactory.dbQuery {
// Qualifikationen werden aktuell nicht in FunktionaerTable gespeichert.
emptyList()
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
emptyList()
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
val query = FunktionaerTable.selectAll().where { FunktionaerTable.vereinsNummer eq vereinsNummer }
if (activeOnly) {
query.andWhere { FunktionaerTable.istAktiv eq true }
}
query.map(::rowToDomFunktionaer)
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map(::rowToDomFunktionaer)
}
override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll()
.limit(limit).offset(offset.toLong())
.map(::rowToDomFunktionaer)
}
override suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer = DatabaseFactory.dbQuery {
val exists = FunktionaerTable.selectAll().where { FunktionaerTable.id eq funktionaer.funktionaerId }.any()
if (exists) {
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
it[richterNummer] = funktionaer.richterNummer
it[vorname] = funktionaer.vorname
it[nachname] = funktionaer.nachname
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name
it[updatedAt] = funktionaer.updatedAt
}
funktionaer
} else {
FunktionaerTable.insert {
it[id] = funktionaer.funktionaerId
it[richterNummer] = funktionaer.richterNummer
it[vorname] = funktionaer.vorname
it[nachname] = funktionaer.nachname
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name
it[createdAt] = funktionaer.createdAt
it[updatedAt] = funktionaer.updatedAt
}
funktionaer
}
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.deleteWhere { FunktionaerTable.id eq id } > 0
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count()
}
override suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean): Long =
DatabaseFactory.dbQuery {
// Aktuell keine Qualifikations-Speicherung
0L
}
override suspend fun existsByRichterNummer(richterNummer: String): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }.any()
}
}
@@ -0,0 +1,177 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.repository.ReiterRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Reiter-Repositorys.
*/
class ExposedReiterRepository : ReiterRepository {
private fun rowToDomReiter(row: ResultRow): DomReiter {
return DomReiter(
reiterId = row[ReiterTable.id],
personId = row[ReiterTable.personId],
satznummer = row[ReiterTable.satznummer],
nachname = row[ReiterTable.nachname],
vorname = row[ReiterTable.vorname],
geburtsdatum = row[ReiterTable.geburtsdatum],
lizenzNummer = row[ReiterTable.lizenzNummer],
lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]),
startkartAktiv = row[ReiterTable.startkartAktiv],
startkartSaison = row[ReiterTable.startkartSaison],
feiId = row[ReiterTable.feiId],
nation = row[ReiterTable.nation],
vereinsNummer = row[ReiterTable.vereinsNummer],
vereinsName = row[ReiterTable.vereinsName],
istGastreiter = row[ReiterTable.istGastreiter],
istAktiv = row[ReiterTable.istAktiv],
datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]),
createdAt = row[ReiterTable.createdAt],
updatedAt = row[ReiterTable.updatedAt]
)
}
override suspend fun findById(id: Uuid): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.id eq id }
.map(::rowToDomReiter)
.singleOrNull()
}
override suspend fun findBySatznummer(satznummer: String): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }
.map(::rowToDomReiter)
.singleOrNull()
}
override suspend fun findByFeiId(feiId: String): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.feiId eq feiId }
.map(::rowToDomReiter)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomReiter> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
ReiterTable.selectAll().where { (ReiterTable.nachname like pattern) or (ReiterTable.vorname like pattern) }
.limit(limit)
.map(::rowToDomReiter)
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomReiter> =
DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.vereinsNummer eq vereinsNummer }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map(::rowToDomReiter)
}
override suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean): List<DomReiter> =
DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.lizenzKlasse eq lizenzKlasse.name }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map(::rowToDomReiter)
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
// Da wir in ReiterTable keinen sparteFilter haben, müssen wir ggf. über eine andere Tabelle gehen
// oder die Logik anpassen. Fürs erste geben wir eine leere Liste zurück oder suchen nach Name in Lizenz?
// TODO: Implementierung prüfen, falls Sparten-Lizenzierung in eigener Tabelle liegt.
emptyList()
}
override suspend fun findGastreiter(activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.istGastreiter eq true }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map(::rowToDomReiter)
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map(::rowToDomReiter)
}
override suspend fun findAll(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll()
.limit(limit).offset(offset.toLong())
.map(::rowToDomReiter)
}
override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
val exists = ReiterTable.selectAll().where { ReiterTable.id eq reiter.reiterId }.any()
if (exists) {
ReiterTable.update({ ReiterTable.id eq reiter.reiterId }) {
it[personId] = reiter.personId
it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname
it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum
it[lizenzNummer] = reiter.lizenzNummer
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[startkartAktiv] = reiter.startkartAktiv
it[startkartSaison] = reiter.startkartSaison
it[feiId] = reiter.feiId
it[nation] = reiter.nation
it[vereinsNummer] = reiter.vereinsNummer
it[vereinsName] = reiter.vereinsName
it[istGastreiter] = reiter.istGastreiter
it[istAktiv] = reiter.istAktiv
it[datenQuelle] = reiter.datenQuelle.name
it[updatedAt] = reiter.updatedAt
}
reiter
} else {
ReiterTable.insert {
it[id] = reiter.reiterId
it[personId] = reiter.personId
it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname
it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum
it[lizenzNummer] = reiter.lizenzNummer
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[startkartAktiv] = reiter.startkartAktiv
it[startkartSaison] = reiter.startkartSaison
it[feiId] = reiter.feiId
it[nation] = reiter.nation
it[vereinsNummer] = reiter.vereinsNummer
it[vereinsName] = reiter.vereinsName
it[istGastreiter] = reiter.istGastreiter
it[istAktiv] = reiter.istAktiv
it[datenQuelle] = reiter.datenQuelle.name
it[createdAt] = reiter.createdAt
it[updatedAt] = reiter.updatedAt
}
reiter
}
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
ReiterTable.deleteWhere { ReiterTable.id eq id } > 0
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }.count()
}
override suspend fun existsBySatznummer(satznummer: String): Boolean = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }.any()
}
}
@@ -0,0 +1,154 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomVerein
import at.mocode.masterdata.domain.repository.VereinRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.jdbc.andWhere
import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Verein-Repositorys.
*/
class ExposedVereinRepository : VereinRepository {
private fun rowToDomVerein(row: ResultRow): DomVerein {
return DomVerein(
vereinId = row[VereinTable.id],
vereinsNummer = row[VereinTable.vereinsNummer],
name = row[VereinTable.name],
kurzname = row[VereinTable.kurzname],
bundesland = row[VereinTable.bundesland],
ort = row[VereinTable.ort],
plz = row[VereinTable.plz],
strasse = row[VereinTable.strasse],
email = row[VereinTable.email],
telefon = row[VereinTable.telefon],
website = row[VereinTable.website],
oepsRegionNummer = row[VereinTable.oepsRegionNummer],
istVeranstalter = row[VereinTable.istVeranstalter],
istAktiv = row[VereinTable.istAktiv],
bemerkungen = row[VereinTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[VereinTable.datenQuelle]),
createdAt = row[VereinTable.createdAt],
updatedAt = row[VereinTable.updatedAt]
)
}
override suspend fun findById(id: Uuid): DomVerein? = DatabaseFactory.dbQuery {
VereinTable.selectAll().where { VereinTable.id eq id }
.map(::rowToDomVerein)
.singleOrNull()
}
override suspend fun findByVereinsNummer(vereinsNummer: String): DomVerein? = DatabaseFactory.dbQuery {
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }
.map(::rowToDomVerein)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
VereinTable.selectAll().where { (VereinTable.name like pattern) or (VereinTable.kurzname like pattern) }
.limit(limit)
.map(::rowToDomVerein)
}
override suspend fun findByBundesland(bundesland: String, activeOnly: Boolean): List<DomVerein> =
DatabaseFactory.dbQuery {
val query = VereinTable.selectAll().where { VereinTable.bundesland eq bundesland }
if (activeOnly) {
query.andWhere { VereinTable.istAktiv eq true }
}
query.map(::rowToDomVerein)
}
override suspend fun findVeranstalter(activeOnly: Boolean): List<DomVerein> = DatabaseFactory.dbQuery {
val query = VereinTable.selectAll().where { VereinTable.istVeranstalter eq true }
if (activeOnly) {
query.andWhere { VereinTable.istAktiv eq true }
}
query.map(::rowToDomVerein)
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> = DatabaseFactory.dbQuery {
VereinTable.selectAll().where { VereinTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map(::rowToDomVerein)
}
override suspend fun findAll(limit: Int, offset: Int): List<DomVerein> = DatabaseFactory.dbQuery {
VereinTable.selectAll()
.limit(limit).offset(offset.toLong())
.map(::rowToDomVerein)
}
override suspend fun save(verein: DomVerein): DomVerein = DatabaseFactory.dbQuery {
val exists = VereinTable.selectAll().where { VereinTable.id eq verein.vereinId }.any()
if (exists) {
VereinTable.update({ VereinTable.id eq verein.vereinId }) {
it[vereinsNummer] = verein.vereinsNummer
it[name] = verein.name
it[kurzname] = verein.kurzname
it[bundesland] = verein.bundesland
it[ort] = verein.ort
it[plz] = verein.plz
it[strasse] = verein.strasse
it[email] = verein.email
it[telefon] = verein.telefon
it[website] = verein.website
it[oepsRegionNummer] = verein.oepsRegionNummer
it[istVeranstalter] = verein.istVeranstalter
it[istAktiv] = verein.istAktiv
it[bemerkungen] = verein.bemerkungen
it[datenQuelle] = verein.datenQuelle.name
it[updatedAt] = verein.updatedAt
}
verein
} else {
VereinTable.insert {
it[id] = verein.vereinId
it[vereinsNummer] = verein.vereinsNummer
it[name] = verein.name
it[kurzname] = verein.kurzname
it[bundesland] = verein.bundesland
it[ort] = verein.ort
it[plz] = verein.plz
it[strasse] = verein.strasse
it[email] = verein.email
it[telefon] = verein.telefon
it[website] = verein.website
it[oepsRegionNummer] = verein.oepsRegionNummer
it[istVeranstalter] = verein.istVeranstalter
it[istAktiv] = verein.istAktiv
it[bemerkungen] = verein.bemerkungen
it[datenQuelle] = verein.datenQuelle.name
it[createdAt] = verein.createdAt
it[updatedAt] = verein.updatedAt
}
verein
}
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
VereinTable.deleteWhere { VereinTable.id eq id } > 0
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
VereinTable.selectAll().where { VereinTable.istAktiv eq true }.count()
}
override suspend fun existsByVereinsNummer(vereinsNummer: String): Boolean = DatabaseFactory.dbQuery {
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }.any()
}
}
@@ -0,0 +1,30 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Funktionär-Entität.
*/
object FunktionaerTable : Table("funktionaer") {
val id = uuid("funktionaer_id")
val richterNummer = varchar("richter_nummer", 10).nullable().uniqueIndex()
val vorname = varchar("vorname", 100)
val nachname = varchar("nachname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val email = varchar("email", 200).nullable()
val telefon = varchar("telefon", 50).nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,286 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.repository.HorseRepository
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Horse-Repositorys.
*/
class HorseRepositoryImpl : HorseRepository {
private fun rowToDomPferd(row: ResultRow): DomPferd {
return DomPferd(
pferdId = row[HorseTable.id],
pferdeName = row[HorseTable.pferdeName],
geschlecht = PferdeGeschlechtE.valueOf(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 = DatenQuelleE.valueOf(row[HorseTable.datenQuelle]),
createdAt = row[HorseTable.createdAt],
updatedAt = row[HorseTable.updatedAt]
)
}
override suspend fun findById(id: Uuid): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.id eq id }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByPassNummer(passNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
HorseTable.selectAll().where { HorseTable.pferdeName like pattern }
.limit(limit)
.map(::rowToDomPferd)
}
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.verantwortlichePersonId eq responsiblePersonId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByGeschlecht(
geschlecht: PferdeGeschlechtE,
activeOnly: Boolean,
limit: Int
): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.geschlecht eq geschlecht.name }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.limit(limit).map(::rowToDomPferd)
}
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> =
DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.rasse eq rasse }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.limit(limit).map(::rowToDomPferd)
}
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
// In Exposed v1 gibt es kein directes year() für date Spalten ohne extra Extension.
// Wir suchen im Datumsbereich.
val startDate = kotlinx.datetime.LocalDate(birthYear, 1, 1)
val endDate = kotlinx.datetime.LocalDate(birthYear, 12, 31)
val query = HorseTable.selectAll()
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
DatabaseFactory.dbQuery {
val startDate = kotlinx.datetime.LocalDate(fromYear, 1, 1)
val endDate = kotlinx.datetime.LocalDate(toYear, 12, 31)
val query = HorseTable.selectAll()
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findAllActive(limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
.limit(limit)
.map(::rowToDomPferd)
}
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun save(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val exists = HorseTable.selectAll().where { HorseTable.id eq horse.pferdId }.any()
if (exists) {
HorseTable.update({ HorseTable.id eq horse.pferdId }) {
it[pferdeName] = horse.pferdeName
it[geschlecht] = horse.geschlecht.name
it[geburtsdatum] = horse.geburtsdatum
it[rasse] = horse.rasse
it[farbe] = horse.farbe
it[besitzerId] = horse.besitzerId
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[zuechterName] = horse.zuechterName
it[zuchtbuchNummer] = horse.zuchtbuchNummer
it[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer
it[passNummer] = horse.passNummer
it[oepsNummer] = horse.oepsNummer
it[feiNummer] = horse.feiNummer
it[vaterName] = horse.vaterName
it[mutterName] = horse.mutterName
it[mutterVaterName] = horse.mutterVaterName
it[stockmass] = horse.stockmass
it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name
it[updatedAt] = horse.updatedAt
}
horse
} else {
HorseTable.insert {
it[id] = horse.pferdId
it[pferdeName] = horse.pferdeName
it[geschlecht] = horse.geschlecht.name
it[geburtsdatum] = horse.geburtsdatum
it[rasse] = horse.rasse
it[farbe] = horse.farbe
it[besitzerId] = horse.besitzerId
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[zuechterName] = horse.zuechterName
it[zuchtbuchNummer] = horse.zuchtbuchNummer
it[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer
it[passNummer] = horse.passNummer
it[oepsNummer] = horse.oepsNummer
it[feiNummer] = horse.feiNummer
it[vaterName] = horse.vaterName
it[mutterName] = horse.mutterName
it[mutterVaterName] = horse.mutterVaterName
it[stockmass] = horse.stockmass
it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name
it[createdAt] = horse.createdAt
it[updatedAt] = horse.updatedAt
}
horse
}
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
HorseTable.deleteWhere { HorseTable.id eq id } > 0
}
override suspend fun existsByLebensnummer(lebensnummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.any()
}
override suspend fun existsByChipNummer(chipNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }.any()
}
override suspend fun existsByPassNummer(passNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }.any()
}
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }.any()
}
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }.any()
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }.count()
}
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
}
@@ -0,0 +1,45 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Pferd-Entität.
*/
object HorseTable : Table("horse") {
val id = uuid("horse_id")
val pferdeName = varchar("pferde_name", 200)
val geschlecht = varchar("geschlecht", 20)
val geburtsdatum = date("geburtsdatum").nullable()
val rasse = varchar("rasse", 100).nullable()
val farbe = varchar("farbe", 100).nullable()
val besitzerId = uuid("besitzer_id").nullable()
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
val zuechterName = varchar("zuechter_name", 200).nullable()
val zuchtbuchNummer = varchar("zuchtbuch_nummer", 50).nullable()
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()
val vaterName = varchar("vater_name", 200).nullable()
val mutterName = varchar("mutter_name", 200).nullable()
val mutterVaterName = varchar("mutter_vater_name", 200).nullable()
val stockmass = integer("stockmass").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_horse_lebensnummer", isUnique = false, lebensnummer)
index("idx_horse_name", isUnique = false, pferdeName)
}
}
@@ -1,28 +1,25 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.LandDefinition
import at.mocode.masterdata.domain.repository.LandRepository
import at.mocode.core.utils.database.DatabaseFactory
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des LandRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der LandDefinition Domain-Entität und der LandTable.
*/
class LandRepositoryImpl : LandRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToLandDefinition(row: ResultRow): LandDefinition {
return LandDefinition(
landId = row[LandTable.id],
@@ -36,8 +33,8 @@ class LandRepositoryImpl : LandRepository {
istEwrMitglied = row[LandTable.istEwrMitglied],
istAktiv = row[LandTable.istAktiv],
sortierReihenfolge = row[LandTable.sortierReihenfolge],
createdAt = row[LandTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[LandTable.updatedAt].toInstant(TimeZone.UTC)
createdAt = row[LandTable.createdAt],
updatedAt = row[LandTable.updatedAt]
)
}
@@ -48,90 +45,82 @@ class LandRepositoryImpl : LandRepository {
}
override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code.uppercase() }
.map(::rowToLandDefinition)
.singleOrNull()
}
override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code.uppercase() }
.map(::rowToLandDefinition)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
LandTable.selectAll().where {
(LandTable.nameDeutsch like pattern) or
(LandTable.nameEnglisch like pattern)
}
.limit(limit)
.map(::rowToLandDefinition)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<LandDefinition> = DatabaseFactory.dbQuery {
val query = LandTable.selectAll().where { LandTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
} else {
query.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
}
query.map(::rowToLandDefinition)
}
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
LandTable.selectAll()
.where { (LandTable.nameDeutsch like pattern) or (LandTable.nameEnglisch like pattern) }
.limit(limit)
.map(::rowToLandDefinition)
}
override suspend fun findEuMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
LandTable.selectAll().where { (LandTable.istEuMitglied eq true) and (LandTable.istAktiv eq true) }
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
LandTable.selectAll().where { LandTable.istEuMitglied eq true }
.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
.map(::rowToLandDefinition)
}
override suspend fun findEwrMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
LandTable.selectAll().where { (LandTable.istEwrMitglied eq true) and (LandTable.istAktiv eq true) }
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
LandTable.selectAll().where { LandTable.istEwrMitglied eq true }
.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
.map(::rowToLandDefinition)
}
override suspend fun save(land: LandDefinition): LandDefinition = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingLand = LandTable.selectAll().where { LandTable.id eq land.landId }.singleOrNull()
if (existingLand == null) {
// Insert a new country
LandTable.insert { stmt ->
stmt[id] = land.landId
stmt[isoAlpha2Code] = land.isoAlpha2Code
stmt[isoAlpha3Code] = land.isoAlpha3Code
stmt[isoNumerischerCode] = land.isoNumerischerCode
stmt[nameDeutsch] = land.nameDeutsch
stmt[nameEnglisch] = land.nameEnglisch
stmt[wappenUrl] = land.wappenUrl
stmt[istEuMitglied] = land.istEuMitglied
stmt[istEwrMitglied] = land.istEwrMitglied
stmt[istAktiv] = land.istAktiv
stmt[sortierReihenfolge] = land.sortierReihenfolge
stmt[createdAt] = land.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
val exists = LandTable.selectAll().where { LandTable.id eq land.landId }.any()
if (exists) {
LandTable.update({ LandTable.id eq land.landId }) {
it[isoAlpha2Code] = land.isoAlpha2Code.uppercase()
it[isoAlpha3Code] = land.isoAlpha3Code.uppercase()
it[isoNumerischerCode] = land.isoNumerischerCode
it[nameDeutsch] = land.nameDeutsch
it[nameEnglisch] = land.nameEnglisch
it[wappenUrl] = land.wappenUrl
it[istEuMitglied] = land.istEuMitglied
it[istEwrMitglied] = land.istEwrMitglied
it[istAktiv] = land.istAktiv
it[sortierReihenfolge] = land.sortierReihenfolge
it[updatedAt] = land.updatedAt
}
land
} else {
// Update existing country
LandTable.update({ LandTable.id eq land.landId }) { stmt ->
stmt[isoAlpha2Code] = land.isoAlpha2Code
stmt[isoAlpha3Code] = land.isoAlpha3Code
stmt[isoNumerischerCode] = land.isoNumerischerCode
stmt[nameDeutsch] = land.nameDeutsch
stmt[nameEnglisch] = land.nameEnglisch
stmt[wappenUrl] = land.wappenUrl
stmt[istEuMitglied] = land.istEuMitglied
stmt[istEwrMitglied] = land.istEwrMitglied
stmt[istAktiv] = land.istAktiv
stmt[sortierReihenfolge] = land.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
LandTable.insert {
it[id] = land.landId
it[isoAlpha2Code] = land.isoAlpha2Code.uppercase()
it[isoAlpha3Code] = land.isoAlpha3Code.uppercase()
it[isoNumerischerCode] = land.isoNumerischerCode
it[nameDeutsch] = land.nameDeutsch
it[nameEnglisch] = land.nameEnglisch
it[wappenUrl] = land.wappenUrl
it[istEuMitglied] = land.istEuMitglied
it[istEwrMitglied] = land.istEwrMitglied
it[istAktiv] = land.istAktiv
it[sortierReihenfolge] = land.sortierReihenfolge
it[createdAt] = land.createdAt
it[updatedAt] = land.updatedAt
}
land
}
land.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
@@ -139,13 +128,11 @@ class LandRepositoryImpl : LandRepository {
}
override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
.count() > 0
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code.uppercase() }.any()
}
override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
.count() > 0
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code.uppercase() }.any()
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
@@ -1,30 +1,28 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Land-Entität (Länderstammdaten).
*
* Diese Tabelle speichert alle Informationen zu Ländern/Nationen entsprechend
* der LandDefinition Domain-Entität.
* Exposed-Tabellendefinition für die Land-Entität.
*/
object LandTable : Table("land") {
val id = javaUUID("id").autoGenerate()
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
val nameDeutsch = varchar("name_deutsch", 100)
val nameEnglisch = varchar("name_englisch", 100).nullable()
val wappenUrl = varchar("wappen_url", 500).nullable()
val istEuMitglied = bool("ist_eu_mitglied").nullable()
val istEwrMitglied = bool("ist_ewr_mitglied").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
val id = uuid("land_id")
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
val nameDeutsch = varchar("name_deutsch", 100)
val nameEnglisch = varchar("name_englisch", 100).nullable()
val wappenUrl = varchar("wappen_url", 255).nullable()
val istEuMitglied = bool("ist_eu_mitglied").nullable()
val istEwrMitglied = bool("ist_ewr_mitglied").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
override val primaryKey = PrimaryKey(id)
}
@@ -2,28 +2,26 @@
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.Platz
import at.mocode.masterdata.domain.repository.PlatzRepository
import at.mocode.core.utils.database.DatabaseFactory
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.jdbc.andWhere
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des PlatzRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der Platz Domain-Entität und der PlatzTable.
* Exposed-basierte Implementierung des Platz-Repositorys.
*/
class PlatzRepositoryImpl : PlatzRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToPlatz(row: ResultRow): Platz {
return Platz(
id = row[PlatzTable.id],
@@ -34,8 +32,8 @@ class PlatzRepositoryImpl : PlatzRepository {
typ = PlatzTypE.valueOf(row[PlatzTable.typ]),
istAktiv = row[PlatzTable.istAktiv],
sortierReihenfolge = row[PlatzTable.sortierReihenfolge],
createdAt = row[PlatzTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[PlatzTable.updatedAt].toInstant(TimeZone.UTC)
createdAt = row[PlatzTable.createdAt],
updatedAt = row[PlatzTable.updatedAt]
)
}
@@ -47,144 +45,102 @@ class PlatzRepositoryImpl : PlatzRepository {
override suspend fun findByTournament(turnierId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.turnierId eq turnierId }
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
if (orderBySortierung) {
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
} else {
query.orderBy(PlatzTable.name to SortOrder.ASC)
}
query.map(::rowToPlatz)
}
override suspend fun findByName(searchTerm: String, turnierId: Uuid?, limit: Int): List<Platz> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
val query = PlatzTable.selectAll().where { PlatzTable.name like pattern }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
query.limit(limit)
.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
turnierId?.let { tid -> query.andWhere { PlatzTable.turnierId eq tid } }
query.limit(limit).map(::rowToPlatz)
}
override suspend fun findByType(typ: PlatzTypE, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.typ eq typ.name }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
turnierId?.let { tid -> query.andWhere { PlatzTable.turnierId eq tid } }
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
query.map(::rowToPlatz)
}
override suspend fun findByGroundType(boden: String, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.boden eq boden }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
turnierId?.let { tid -> query.andWhere { PlatzTable.turnierId eq tid } }
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
query.map(::rowToPlatz)
}
override suspend fun findByDimensions(dimension: String, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.dimension eq dimension }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
turnierId?.let { tid -> query.andWhere { PlatzTable.turnierId eq tid } }
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
query.map(::rowToPlatz)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
} else {
query.orderBy(PlatzTable.name to SortOrder.ASC)
}
query.map(::rowToPlatz)
}
override suspend fun findSuitableForDiscipline(
requiredType: PlatzTypE,
requiredDimensions: String?,
turnierId: Uuid?
): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where {
(PlatzTable.typ eq requiredType.name) and (PlatzTable.istAktiv eq true)
}
requiredDimensions?.let { dimensions ->
query.andWhere { PlatzTable.dimension eq dimensions }
}
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
override suspend fun findSuitableForDiscipline(
requiredType: PlatzTypE,
requiredDimensions: String?,
turnierId: Uuid?
): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.typ eq requiredType.name }
requiredDimensions?.let { dim -> query.andWhere { PlatzTable.dimension eq dim } }
turnierId?.let { tid -> query.andWhere { PlatzTable.turnierId eq tid } }
query.andWhere { PlatzTable.istAktiv eq true }
query.map(::rowToPlatz)
}
override suspend fun save(platz: Platz): Platz = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingPlatz = PlatzTable.selectAll().where { PlatzTable.id eq platz.id }.singleOrNull()
if (existingPlatz == null) {
// Insert a new venue
PlatzTable.insert { stmt ->
stmt[id] = platz.id
stmt[turnierId] = platz.turnierId
stmt[name] = platz.name
stmt[dimension] = platz.dimension
stmt[boden] = platz.boden
stmt[typ] = platz.typ.name
stmt[istAktiv] = platz.istAktiv
stmt[sortierReihenfolge] = platz.sortierReihenfolge
stmt[createdAt] = platz.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
val exists = PlatzTable.selectAll().where { PlatzTable.id eq platz.id }.any()
if (exists) {
PlatzTable.update({ PlatzTable.id eq platz.id }) {
it[turnierId] = platz.turnierId
it[name] = platz.name
it[dimension] = platz.dimension
it[boden] = platz.boden
it[typ] = platz.typ.name
it[istAktiv] = platz.istAktiv
it[sortierReihenfolge] = platz.sortierReihenfolge
it[updatedAt] = platz.updatedAt
}
platz
} else {
// Update existing venue
PlatzTable.update({ PlatzTable.id eq platz.id }) { stmt ->
stmt[turnierId] = platz.turnierId
stmt[name] = platz.name
stmt[dimension] = platz.dimension
stmt[boden] = platz.boden
stmt[typ] = platz.typ.name
stmt[istAktiv] = platz.istAktiv
stmt[sortierReihenfolge] = platz.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
PlatzTable.insert {
it[id] = platz.id
it[turnierId] = platz.turnierId
it[name] = platz.name
it[dimension] = platz.dimension
it[boden] = platz.boden
it[typ] = platz.typ.name
it[istAktiv] = platz.istAktiv
it[sortierReihenfolge] = platz.sortierReihenfolge
it[createdAt] = platz.createdAt
it[updatedAt] = platz.updatedAt
}
platz
}
platz.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
@@ -192,40 +148,24 @@ class PlatzRepositoryImpl : PlatzRepository {
}
override suspend fun existsByNameAndTournament(name: String, turnierId: Uuid): Boolean = DatabaseFactory.dbQuery {
PlatzTable.selectAll().where {
(PlatzTable.name eq name) and (PlatzTable.turnierId eq turnierId)
}.count() > 0
PlatzTable.selectAll().where { (PlatzTable.name eq name) and (PlatzTable.turnierId eq turnierId) }.any()
}
override suspend fun countActiveByTournament(turnierId: Uuid): Long = DatabaseFactory.dbQuery {
PlatzTable.selectAll().where {
(PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true)
}.count()
PlatzTable.selectAll().where { (PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true) }.count()
}
override suspend fun countByTypeAndTournament(typ: PlatzTypE, turnierId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where {
(PlatzTable.typ eq typ.name) and (PlatzTable.turnierId eq turnierId)
}
val query = PlatzTable.selectAll().where { (PlatzTable.turnierId eq turnierId) and (PlatzTable.typ eq typ.name) }
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.count()
}
override suspend fun findAvailableForTimeSlot(turnierId: Uuid, startTime: String?, endTime: String?): List<Platz> = DatabaseFactory.dbQuery {
// For now, this returns all active venues for the tournament
// This can be extended when venue scheduling functionality is implemented
val query = PlatzTable.selectAll().where {
(PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true)
}
// TODO: Add time slot availability logic when scheduling is implemented
// This would involve joining with a scheduling/booking table to check availability
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
// Derzeit gibt die Methode einfach alle aktiven Plätze des Turniers zurück.
PlatzTable.selectAll().where { (PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true) }
.map(::rowToPlatz)
}
}
@@ -1,38 +1,29 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.v1.core.javaUUID
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Platz-Entität (Turnierplätze/Wettkampfstätten).
*
* Diese Tabelle speichert alle Informationen zu Plätzen und Arenen
* entsprechend der Platz Domain-Entität.
* Exposed-Tabellendefinition für die Platz-Entität.
*/
object PlatzTable : Table("platz") {
val id = javaUUID("id").autoGenerate()
val turnierId = javaUUID("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module)
val name = varchar("name", 200)
val dimension = varchar("dimension", 50).nullable()
val boden = varchar("boden", 100).nullable()
val typ = varchar("typ", 50) // Enum as string
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
val id = uuid("platz_id")
val turnierId = uuid("turnier_id")
val name = varchar("name", 100)
val dimension = varchar("dimension", 50).nullable()
val boden = varchar("boden", 50).nullable()
val typ = varchar("typ", 50)
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
override val primaryKey = PrimaryKey(id)
init {
// Index for performance on common queries
index(customIndexName = "idx_platz_turnier", columns = arrayOf(turnierId))
index(customIndexName = "idx_platz_aktiv", columns = arrayOf(istAktiv))
index(customIndexName = "idx_platz_typ", columns = arrayOf(typ))
index(customIndexName = "idx_platz_turnier_aktiv", columns = arrayOf(turnierId, istAktiv))
// Unique constraint for name per tournament
uniqueIndex("uk_platz_name_turnier", name, turnierId)
}
init {
index("idx_platz_turnier", isUnique = false, turnierId)
}
}
@@ -0,0 +1,40 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Reiter-Entität.
*/
object ReiterTable : Table("reiter") {
val id = uuid("reiter_id")
val personId = uuid("person_id")
val satznummer = varchar("satznummer", 10).uniqueIndex()
val lizenzNummer = varchar("lizenz_nummer", 20).nullable()
val lizenzKlasse = varchar("lizenz_klasse", 20)
val startkartAktiv = bool("startkart_aktiv").default(false)
val startkartSaison = integer("startkart_saison").nullable()
val feiId = varchar("fei_id", 20).nullable()
val nation = varchar("nation", 3).nullable()
val nachname = varchar("nachname", 100)
val vorname = varchar("vorname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val vereinsName = varchar("vereins_name", 200).nullable()
val istGastreiter = bool("ist_gastreiter").default(false)
val istAktiv = bool("ist_aktiv").default(true)
val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_reiter_satznummer", isUnique = true, satznummer)
index("idx_reiter_name", isUnique = false, nachname, vorname)
}
}
@@ -0,0 +1,33 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Verein-Entität.
*/
object VereinTable : Table("verein") {
val id = uuid("verein_id")
val vereinsNummer = varchar("vereins_nummer", 10).uniqueIndex()
val name = varchar("name", 200)
val kurzname = varchar("kurzname", 100).nullable()
val bundesland = varchar("bundesland", 100).nullable()
val ort = varchar("ort", 100).nullable()
val plz = varchar("plz", 10).nullable()
val strasse = varchar("strasse", 200).nullable()
val email = varchar("email", 200).nullable()
val telefon = varchar("telefon", 50).nullable()
val website = varchar("website", 255).nullable()
val oepsRegionNummer = varchar("oeps_region_nummer", 10).nullable()
val istVeranstalter = bool("ist_veranstalter").default(false)
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
@@ -1,57 +1,53 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
alias(libs.plugins.spring.boot)
// Dependency Management für konsistente Spring-Versionen
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinSpring)
}
// Dieser Block funktioniert jetzt, weil das `springBoot`-Plugin oben aktiviert ist.
springBoot {
mainClass.set("at.mocode.masterdata.service.MasterdataServiceApplicationKt")
mainClass.set("at.mocode.masterdata.service.MasterdataServiceApplicationKt")
}
dependencies {
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreUtils)
implementation(projects.masterdata.masterdataDomain)
implementation(projects.masterdata.masterdataApplication)
implementation(projects.masterdata.masterdataInfrastructure)
implementation(projects.masterdata.masterdataApi)
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreUtils)
implementation(projects.backend.services.masterdata.masterdataDomain)
implementation(projects.backend.services.masterdata.masterdataInfrastructure)
implementation(projects.backend.services.masterdata.masterdataCommon)
implementation(projects.backend.services.masterdata.masterdataApi)
// Infrastruktur-Clients
implementation(projects.infrastructure.cache.redisCache)
implementation(projects.infrastructure.messaging.messagingClient)
implementation(projects.infrastructure.monitoring.monitoringClient)
// Infrastruktur-Clients
implementation(projects.backend.infrastructure.cache.valkeyCache)
implementation(projects.backend.infrastructure.messaging.messagingClient)
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
// Spring Boot Starters
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
//implementation(libs.springdoc.openapi.starter.webmvc.ui)
// Spring Boot Starters
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
//implementation(libs.springdoc.openapi.starter.webmvc.ui)
// Datenbank-Abhängigkeiten
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlin.datetime)
implementation(libs.hikari.cp)
runtimeOnly(libs.postgresql.driver)
testRuntimeOnly(libs.h2.driver)
// Datenbank-Abhängigkeiten
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlin.datetime)
implementation(libs.hikari.cp)
// implementation(libs.firebase.database.ktx) // Firebase removed
runtimeOnly(libs.postgresql.driver)
testRuntimeOnly(libs.h2.driver)
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.logback.classic) // SLF4J provider for tests
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.logback.classic) // SLF4J provider for tests
}
tasks.test {
useJUnitPlatform()
useJUnitPlatform()
}
@@ -1,7 +1,5 @@
package at.mocode.masterdata.service
import at.mocode.core.utils.config.AppConfig
import at.mocode.core.utils.database.DatabaseFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@@ -14,17 +12,6 @@ import org.springframework.boot.runApplication
class MasterdataServiceApplication
fun main(args: Array<String>) {
// 1. Lade die Konfiguration explizit, genau einmal beim Start.
val appConfig = AppConfig.load()
println("Konfiguration für Umgebung '${appConfig.environment}' geladen.")
// 2. Initialisiere die Datenbank mit der geladenen Konfiguration.
// Flyway-Migrationen werden hier automatisch ausgeführt.
DatabaseFactory.init(appConfig.database)
println("Datenbank initialisiert und migriert.")
// 3. Starte die Spring Boot / Ktor Anwendung.
// Der appConfig-Wert kann hier an die Anwendung übergeben werden,
// um ihn später per Dependency Injection zu nutzen.
runApplication<MasterdataServiceApplication>(*args)
// Starte die Spring Boot Anwendung.
runApplication<MasterdataServiceApplication>(*args)
}
@@ -17,103 +17,123 @@ import org.springframework.context.annotation.Profile
@Configuration
class MasterdataConfiguration {
// Repository Implementations
@Bean
fun landRepository(): LandRepository {
return LandRepositoryImpl()
}
// Repository Implementations
@Bean
fun landRepository(): LandRepository {
return LandRepositoryImpl()
}
@Bean
fun bundeslandRepository(): BundeslandRepository {
return BundeslandRepositoryImpl()
}
@Bean
fun bundeslandRepository(): BundeslandRepository {
return BundeslandRepositoryImpl()
}
@Bean
fun altersklasseRepository(): AltersklasseRepository {
return AltersklasseRepositoryImpl()
}
@Bean
fun altersklasseRepository(): AltersklasseRepository {
return AltersklasseRepositoryImpl()
}
@Bean
fun platzRepository(): PlatzRepository {
return PlatzRepositoryImpl()
}
@Bean
fun platzRepository(): PlatzRepository {
return PlatzRepositoryImpl()
}
// Use Cases - Country/Land
@Bean
fun getCountryUseCase(landRepository: LandRepository): GetCountryUseCase {
return GetCountryUseCase(landRepository)
}
@Bean
fun reiterRepository(): ReiterRepository {
return ExposedReiterRepository()
}
@Bean
fun createCountryUseCase(landRepository: LandRepository): CreateCountryUseCase {
return CreateCountryUseCase(landRepository)
}
@Bean
fun vereinRepository(): VereinRepository {
return ExposedVereinRepository()
}
// Use Cases - Federal State/Bundesland
@Bean
fun getBundeslandUseCase(bundeslandRepository: BundeslandRepository): GetBundeslandUseCase {
return GetBundeslandUseCase(bundeslandRepository)
}
@Bean
fun horseRepository(): HorseRepository {
return HorseRepositoryImpl()
}
@Bean
fun createBundeslandUseCase(bundeslandRepository: BundeslandRepository): CreateBundeslandUseCase {
return CreateBundeslandUseCase(bundeslandRepository)
}
@Bean
fun funktionaerRepository(): FunktionaerRepository {
return ExposedFunktionaerRepository()
}
// Use Cases - Age Class/Altersklasse
@Bean
fun getAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): GetAltersklasseUseCase {
return GetAltersklasseUseCase(altersklasseRepository)
}
// Use Cases - Country/Land
@Bean
fun getCountryUseCase(landRepository: LandRepository): GetCountryUseCase {
return GetCountryUseCase(landRepository)
}
@Bean
fun createAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): CreateAltersklasseUseCase {
return CreateAltersklasseUseCase(altersklasseRepository)
}
@Bean
fun createCountryUseCase(landRepository: LandRepository): CreateCountryUseCase {
return CreateCountryUseCase(landRepository)
}
// Use Cases - Venue/Platz
@Bean
fun getPlatzUseCase(platzRepository: PlatzRepository): GetPlatzUseCase {
return GetPlatzUseCase(platzRepository)
}
// Use Cases - Federal State/Bundesland
@Bean
fun getBundeslandUseCase(bundeslandRepository: BundeslandRepository): GetBundeslandUseCase {
return GetBundeslandUseCase(bundeslandRepository)
}
@Bean
fun createPlatzUseCase(platzRepository: PlatzRepository): CreatePlatzUseCase {
return CreatePlatzUseCase(platzRepository)
}
@Bean
fun createBundeslandUseCase(bundeslandRepository: BundeslandRepository): CreateBundeslandUseCase {
return CreateBundeslandUseCase(bundeslandRepository)
}
// API Controllers
@Bean
fun countryController(
getCountryUseCase: GetCountryUseCase,
createCountryUseCase: CreateCountryUseCase
): CountryController {
return CountryController(getCountryUseCase, createCountryUseCase)
}
// Use Cases - Age Class/Altersklasse
@Bean
fun getAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): GetAltersklasseUseCase {
return GetAltersklasseUseCase(altersklasseRepository)
}
@Bean
fun bundeslandController(
getBundeslandUseCase: GetBundeslandUseCase,
createBundeslandUseCase: CreateBundeslandUseCase
): BundeslandController {
return BundeslandController(getBundeslandUseCase, createBundeslandUseCase)
}
@Bean
fun createAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): CreateAltersklasseUseCase {
return CreateAltersklasseUseCase(altersklasseRepository)
}
@Bean
fun altersklasseController(
getAltersklasseUseCase: GetAltersklasseUseCase,
createAltersklasseUseCase: CreateAltersklasseUseCase
): AltersklasseController {
return AltersklasseController(getAltersklasseUseCase, createAltersklasseUseCase)
}
// Use Cases - Venue/Platz
@Bean
fun getPlatzUseCase(platzRepository: PlatzRepository): GetPlatzUseCase {
return GetPlatzUseCase(platzRepository)
}
@Bean
fun platzController(
getPlatzUseCase: GetPlatzUseCase,
createPlatzUseCase: CreatePlatzUseCase
): PlatzController {
return PlatzController(getPlatzUseCase, createPlatzUseCase)
}
@Bean
fun createPlatzUseCase(platzRepository: PlatzRepository): CreatePlatzUseCase {
return CreatePlatzUseCase(platzRepository)
}
// API Controllers
@Bean
fun countryController(
getCountryUseCase: GetCountryUseCase,
createCountryUseCase: CreateCountryUseCase
): CountryController {
return CountryController(getCountryUseCase, createCountryUseCase)
}
@Bean
fun bundeslandController(
getBundeslandUseCase: GetBundeslandUseCase,
createBundeslandUseCase: CreateBundeslandUseCase
): BundeslandController {
return BundeslandController(getBundeslandUseCase, createBundeslandUseCase)
}
@Bean
fun altersklasseController(
getAltersklasseUseCase: GetAltersklasseUseCase,
createAltersklasseUseCase: CreateAltersklasseUseCase
): AltersklasseController {
return AltersklasseController(getAltersklasseUseCase, createAltersklasseUseCase)
}
@Bean
fun platzController(
getPlatzUseCase: GetPlatzUseCase,
createPlatzUseCase: CreatePlatzUseCase
): PlatzController {
return PlatzController(getPlatzUseCase, createPlatzUseCase)
}
}
/**
@@ -122,33 +142,33 @@ class MasterdataConfiguration {
@Configuration
class DatabaseConfiguration {
/**
* Development database configuration.
*/
@Configuration
@Profile("dev", "development")
class DevelopmentDatabaseConfig {
// Development-specific database configuration
// This would typically include H2 or local PostgreSQL setup
}
/**
* Development database configuration.
*/
@Configuration
@Profile("dev", "development")
class DevelopmentDatabaseConfig {
// Development-specific database configuration
// This would typically include H2 or local PostgreSQL setup
}
/**
* Production database configuration.
*/
@Configuration
@Profile("prod", "production")
class ProductionDatabaseConfig {
// Production-specific database configuration
// This would include production PostgreSQL setup with connection pooling
}
/**
* Production database configuration.
*/
@Configuration
@Profile("prod", "production")
class ProductionDatabaseConfig {
// Production-specific database configuration
// This would include production PostgreSQL setup with connection pooling
}
/**
* Test database configuration.
*/
@Configuration
@Profile("test")
class TestDatabaseConfig {
// Test-specific database configuration
// This would typically include in-memory H2 database
}
/**
* Test database configuration.
*/
@Configuration
@Profile("test")
class TestDatabaseConfig {
// Test-specific database configuration
// This would typically include in-memory H2 database
}
}
@@ -1,18 +1,17 @@
package at.mocode.masterdata.service.config
import at.mocode.core.utils.database.DatabaseConfig
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.infrastructure.persistence.LandTable
import at.mocode.masterdata.infrastructure.persistence.BundeslandTable
import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable
import at.mocode.masterdata.infrastructure.persistence.BundeslandTable
import at.mocode.masterdata.infrastructure.persistence.LandTable
import at.mocode.masterdata.infrastructure.persistence.PlatzTable
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.LoggerFactory
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
/**
* Database configuration for the Masterdata Service.
@@ -24,40 +23,33 @@ import org.jetbrains.exposed.sql.transactions.transaction
@Profile("!test")
class MasterdataDatabaseConfiguration {
private val log = LoggerFactory.getLogger(MasterdataDatabaseConfiguration::class.java)
private val log = LoggerFactory.getLogger(MasterdataDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
log.info("Initializing database schema for Masterdata Service...")
@PostConstruct
fun initializeDatabase() {
log.info("Initializing database schema for Masterdata Service...")
try {
// Database connection is already initialized by the gateway
// Only initialize the schema for this service
transaction {
SchemaUtils.create(
LandTable,
BundeslandTable,
AltersklasseTable,
PlatzTable
)
log.info("Masterdata database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize database schema", e)
throw e
}
try {
// Database connection should be initialized by Spring Boot
transaction {
SchemaUtils.create(
LandTable,
BundeslandTable,
AltersklasseTable,
PlatzTable
)
log.info("Masterdata database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize database schema", e)
throw e
}
}
@PreDestroy
fun closeDatabase() {
log.info("Closing database connection for Masterdata Service...")
try {
DatabaseFactory.close()
log.info("Database connection closed successfully")
} catch (e: Exception) {
log.error("Error closing database connection", e)
}
}
@PreDestroy
fun closeDatabase() {
log.info("Closing Masterdata Service database configuration...")
}
}
/**
@@ -67,51 +59,31 @@ class MasterdataDatabaseConfiguration {
@Profile("test")
class MasterdataTestDatabaseConfiguration {
private val log = LoggerFactory.getLogger(MasterdataTestDatabaseConfiguration::class.java)
private val log = LoggerFactory.getLogger(MasterdataTestDatabaseConfiguration::class.java)
@PostConstruct
fun initializeTestDatabase() {
log.info("Initializing test database connection for Masterdata Service...")
@PostConstruct
fun initializeTestDatabase() {
log.info("Initializing test database schema for Masterdata Service...")
try {
// Use H2 in-memory database for tests
val testConfig = DatabaseConfig(
jdbcUrl = "jdbc:h2:mem:masterdata_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
username = "sa",
password = "",
driverClassName = "org.h2.Driver",
maxPoolSize = 5,
minPoolSize = 1,
autoMigrate = true
)
DatabaseFactory.init(testConfig)
log.info("Test database connection initialized successfully")
// Initialize database schema for tests
transaction {
SchemaUtils.create(
LandTable,
BundeslandTable,
AltersklasseTable,
PlatzTable
)
log.info("Test masterdata database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize test database connection", e)
throw e
}
try {
// Initialize database schema for tests
transaction {
SchemaUtils.create(
LandTable,
BundeslandTable,
AltersklasseTable,
PlatzTable
)
log.info("Test masterdata database schema initialized successfully")
}
} catch (e: Exception) {
log.error("Failed to initialize test database schema", e)
throw e
}
}
@PreDestroy
fun closeTestDatabase() {
log.info("Closing test database connection for Masterdata Service...")
try {
DatabaseFactory.close()
log.info("Test database connection closed successfully")
} catch (e: Exception) {
log.error("Error closing test database connection", e)
}
}
@PreDestroy
fun closeTestDatabase() {
log.info("Closing test database configuration for Masterdata Service...")
}
}