(vision) SCS/DDD
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package at.mocode
|
||||
|
||||
import at.mocode.config.ServiceConfiguration
|
||||
import at.mocode.plugins.configureDatabase
|
||||
import at.mocode.plugins.configureRouting
|
||||
import at.mocode.utils.ApiResponse
|
||||
import at.mocode.validation.ValidationException
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
@@ -25,6 +28,11 @@ fun main(args: Array<String>) {
|
||||
fun Application.module() {
|
||||
val log = LoggerFactory.getLogger("Application")
|
||||
log.info("Initializing application...")
|
||||
|
||||
// Configure dependency injection
|
||||
ServiceConfiguration.configureServices()
|
||||
log.info("Services configured")
|
||||
|
||||
configureDatabase()
|
||||
configurePlugins()
|
||||
configureRouting()
|
||||
@@ -94,17 +102,61 @@ private fun Application.configurePlugins() {
|
||||
|
||||
// Configure status pages for error handling
|
||||
install(StatusPages) {
|
||||
exception<Throwable> { call, cause ->
|
||||
call.respondText(
|
||||
text = "500: ${cause.message ?: "Internal Server Error"}",
|
||||
status = HttpStatusCode.InternalServerError
|
||||
// Handle validation exceptions with detailed error information
|
||||
exception<ValidationException> { call, cause ->
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<Nothing>(
|
||||
success = false,
|
||||
error = "VALIDATION_ERROR",
|
||||
message = "Validation failed: ${cause.validationResult.errors.joinToString(", ") { "${it.field}: ${it.message}" }}"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Handle illegal argument exceptions (typically validation-related)
|
||||
exception<IllegalArgumentException> { call, cause ->
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<Nothing>(
|
||||
success = false,
|
||||
error = "INVALID_INPUT",
|
||||
message = cause.message ?: "Invalid input provided"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Handle not found exceptions
|
||||
exception<NoSuchElementException> { call, cause ->
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
ApiResponse<Nothing>(
|
||||
success = false,
|
||||
error = "NOT_FOUND",
|
||||
message = cause.message ?: "Resource not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Handle all other exceptions
|
||||
exception<Throwable> { call, cause ->
|
||||
this@configurePlugins.log.error("Unhandled exception", cause)
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse<Nothing>(
|
||||
success = false,
|
||||
error = "INTERNAL_ERROR",
|
||||
message = "An internal server error occurred"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Handle 404 status
|
||||
status(HttpStatusCode.NotFound) { call, _ ->
|
||||
call.respondText(
|
||||
text = "404: Page Not Found",
|
||||
status = HttpStatusCode.NotFound
|
||||
"404: Page Not Found",
|
||||
ContentType.Text.Plain,
|
||||
HttpStatusCode.NotFound
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package at.mocode.config
|
||||
|
||||
import at.mocode.di.ServiceRegistry
|
||||
import at.mocode.di.register
|
||||
import at.mocode.di.resolve
|
||||
import at.mocode.repositories.*
|
||||
import at.mocode.services.*
|
||||
|
||||
/**
|
||||
* Configuration class for setting up dependency injection using ServiceLocator.
|
||||
* Registers all repositories and services with the ServiceRegistry.
|
||||
*/
|
||||
object ServiceConfiguration {
|
||||
|
||||
/**
|
||||
* Initialize and configure all services and repositories
|
||||
*/
|
||||
fun configureServices() {
|
||||
val serviceLocator = ServiceRegistry.serviceLocator
|
||||
|
||||
// Register repositories
|
||||
registerRepositories(serviceLocator)
|
||||
|
||||
// Register services
|
||||
registerServices(serviceLocator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all repository implementations
|
||||
*/
|
||||
private fun registerRepositories(serviceLocator: at.mocode.di.ServiceLocator) {
|
||||
// Register repository implementations
|
||||
serviceLocator.register<PersonRepository> { PostgresPersonRepository() }
|
||||
serviceLocator.register<PlatzRepository> { PostgresPlatzRepository() }
|
||||
serviceLocator.register<VereinRepository> { PostgresVereinRepository() }
|
||||
serviceLocator.register<ArtikelRepository> { PostgresArtikelRepository() }
|
||||
serviceLocator.register<AbteilungRepository> { PostgresAbteilungRepository() }
|
||||
serviceLocator.register<BewerbRepository> { PostgresBewerbRepository() }
|
||||
serviceLocator.register<DomLizenzRepository> { PostgresDomLizenzRepository() }
|
||||
serviceLocator.register<DomPferdRepository> { PostgresDomPferdRepository() }
|
||||
serviceLocator.register<DomQualifikationRepository> { PostgresDomQualifikationRepository() }
|
||||
serviceLocator.register<TurnierRepository> { PostgresTurnierRepository() }
|
||||
serviceLocator.register<VeranstaltungRepository> { PostgresVeranstaltungRepository() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all service implementations
|
||||
*/
|
||||
private fun registerServices(serviceLocator: at.mocode.di.ServiceLocator) {
|
||||
// Register services with their dependencies
|
||||
serviceLocator.register<PersonService> {
|
||||
PersonService(serviceLocator.resolve<PersonRepository>())
|
||||
}
|
||||
serviceLocator.register<PlatzService> {
|
||||
PlatzService(serviceLocator.resolve<PlatzRepository>())
|
||||
}
|
||||
serviceLocator.register<VereinService> {
|
||||
VereinService(serviceLocator.resolve<VereinRepository>())
|
||||
}
|
||||
serviceLocator.register<ArtikelService> {
|
||||
ArtikelService(serviceLocator.resolve<ArtikelRepository>())
|
||||
}
|
||||
serviceLocator.register<AbteilungService> {
|
||||
AbteilungService(serviceLocator.resolve<AbteilungRepository>())
|
||||
}
|
||||
serviceLocator.register<BewerbService> {
|
||||
BewerbService(serviceLocator.resolve<BewerbRepository>())
|
||||
}
|
||||
serviceLocator.register<DomLizenzService> {
|
||||
DomLizenzService(serviceLocator.resolve<DomLizenzRepository>())
|
||||
}
|
||||
serviceLocator.register<DomPferdService> {
|
||||
DomPferdService(serviceLocator.resolve<DomPferdRepository>())
|
||||
}
|
||||
serviceLocator.register<DomQualifikationService> {
|
||||
DomQualifikationService(serviceLocator.resolve<DomQualifikationRepository>())
|
||||
}
|
||||
serviceLocator.register<TurnierService> {
|
||||
TurnierService(serviceLocator.resolve<TurnierRepository>())
|
||||
}
|
||||
serviceLocator.register<VeranstaltungService> {
|
||||
VeranstaltungService(serviceLocator.resolve<VeranstaltungRepository>())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered services (useful for testing)
|
||||
*/
|
||||
fun clearServices() {
|
||||
ServiceRegistry.serviceLocator.clear()
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,12 @@ val VersioningPlugin = createApplicationPlugin(name = "VersioningPlugin") {
|
||||
}
|
||||
|
||||
/**
|
||||
* Key for storing client version in call attributes
|
||||
* Key for storing a client version in call attributes
|
||||
*/
|
||||
val ClientVersionKey = AttributeKey<String>("ClientVersion")
|
||||
|
||||
/**
|
||||
* Extension function to get client version from call
|
||||
* Extension function to get a client version from call
|
||||
*/
|
||||
fun ApplicationCall.getClientVersion(): String {
|
||||
return attributes.getOrNull(ClientVersionKey) ?: VersionManager.CURRENT_API_VERSION
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.datetime.Instant
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
@@ -58,7 +59,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
* Optimized findById - uses select with where clause directly
|
||||
*/
|
||||
protected open suspend fun findById(id: Uuid): T? = transaction {
|
||||
table.select { getIdColumn() eq id }
|
||||
table.selectAll().where { getIdColumn() eq id }
|
||||
.map { rowToModel(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
@@ -67,7 +68,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
* Generic find by column with single result
|
||||
*/
|
||||
protected suspend fun <V> findByColumn(column: Column<V>, value: V): T? = transaction {
|
||||
table.select { column eq value }
|
||||
table.selectAll().where { column eq value }
|
||||
.map { rowToModel(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
@@ -76,7 +77,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
* Generic find by column with multiple results
|
||||
*/
|
||||
protected suspend fun <V> findByColumnList(column: Column<V>, value: V): List<T> = transaction {
|
||||
table.select { column eq value }
|
||||
table.selectAll().where { column eq value }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
*/
|
||||
protected suspend fun findByLikeSearch(column: Column<String?>, searchTerm: String): List<T> = transaction {
|
||||
val sanitizedTerm = searchTerm.replace("%", "\\%").replace("_", "\\_")
|
||||
table.select { column like "%$sanitizedTerm%" }
|
||||
table.selectAll().where { column like "%$sanitizedTerm%" }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
*/
|
||||
protected suspend fun findByLikeSearchNonNull(column: Column<String>, searchTerm: String): List<T> = transaction {
|
||||
val sanitizedTerm = searchTerm.replace("%", "\\%").replace("_", "\\_")
|
||||
table.select { column like "%$sanitizedTerm%" }
|
||||
table.selectAll().where { column like "%$sanitizedTerm%" }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
|
||||
@@ -117,7 +118,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
}
|
||||
}
|
||||
|
||||
table.select { combinedCondition!! }
|
||||
table.selectAll().where { combinedCondition!! }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
* Find by boolean column (e.g., active status)
|
||||
*/
|
||||
protected suspend fun findByBooleanColumn(column: Column<Boolean>, value: Boolean): List<T> = transaction {
|
||||
table.select { column eq value }
|
||||
table.selectAll().where { column eq value }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
* Find by integer column
|
||||
*/
|
||||
protected suspend fun findByIntColumn(column: Column<Int>, value: Int): List<T> = transaction {
|
||||
table.select { column eq value }
|
||||
table.selectAll().where { column eq value }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
|
||||
@@ -174,7 +175,7 @@ abstract class BaseRepository<T, TTable : Table>(
|
||||
* Find by nullable integer column
|
||||
*/
|
||||
protected suspend fun findByNullableIntColumn(column: Column<Int?>, value: Int): List<T> = transaction {
|
||||
table.select { column eq value }
|
||||
table.selectAll().where { column eq value }
|
||||
.map { rowToModel(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package at.mocode.repositories
|
||||
|
||||
import at.mocode.model.Platz
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
interface PlatzRepository {
|
||||
suspend fun findAll(): List<Platz>
|
||||
suspend fun findById(id: Uuid): Platz?
|
||||
suspend fun findByTurnierId(turnierId: Uuid): List<Platz>
|
||||
suspend fun findByTyp(typ: at.mocode.enums.PlatzTypE): List<Platz>
|
||||
suspend fun create(platz: Platz): Platz
|
||||
suspend fun update(id: Uuid, platz: Platz): Platz?
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
suspend fun search(query: String): List<Platz>
|
||||
}
|
||||
@@ -102,8 +102,8 @@ class PostgresAbteilungRepository : AbteilungRepository {
|
||||
override suspend fun search(query: String): List<Abteilung> = transaction {
|
||||
AbteilungTable.selectAll().where {
|
||||
(AbteilungTable.abteilungsKennzeichen.lowerCase() like "%${query.lowercase()}%") or
|
||||
(AbteilungTable.bezeichnungIntern?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE) or
|
||||
(AbteilungTable.bezeichnungAufStartliste?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE)
|
||||
AbteilungTable.bezeichnungIntern.lowerCase().like("%${query.lowercase()}%") or
|
||||
AbteilungTable.bezeichnungAufStartliste.lowerCase().like("%${query.lowercase()}%")
|
||||
}.map { rowToAbteilung(it) }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package at.mocode.repositories
|
||||
|
||||
import at.mocode.enums.FunktionaerRolle
|
||||
import at.mocode.enums.FunktionaerRolleE
|
||||
import at.mocode.stammdaten.Person
|
||||
import at.mocode.tables.stammdaten.PersonenTable
|
||||
import com.benasher44.uuid.Uuid
|
||||
@@ -139,14 +139,14 @@ class PostgresPersonRepository : PersonRepository {
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseRollen(rollenCsv: String?): Set<FunktionaerRolle> {
|
||||
private fun parseRollen(rollenCsv: String?): Set<FunktionaerRolleE> {
|
||||
return if (rollenCsv.isNullOrBlank()) {
|
||||
emptySet()
|
||||
} else {
|
||||
rollenCsv.split(",")
|
||||
.mapNotNull { roleName ->
|
||||
try {
|
||||
FunktionaerRolle.valueOf(roleName.trim())
|
||||
FunktionaerRolleE.valueOf(roleName.trim())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package at.mocode.repositories
|
||||
|
||||
import at.mocode.model.Platz
|
||||
import at.mocode.tables.PlaetzeTable
|
||||
import at.mocode.enums.PlatzTypE
|
||||
import com.benasher44.uuid.Uuid
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
class PostgresPlatzRepository : PlatzRepository {
|
||||
|
||||
override suspend fun findAll(): List<Platz> = transaction {
|
||||
PlaetzeTable.selectAll().map { rowToPlatz(it) }
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): Platz? = transaction {
|
||||
PlaetzeTable.selectAll().where { PlaetzeTable.id eq id }
|
||||
.map { rowToPlatz(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByTurnierId(turnierId: Uuid): List<Platz> = transaction {
|
||||
PlaetzeTable.selectAll().where { PlaetzeTable.turnierId eq turnierId }
|
||||
.map { rowToPlatz(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByTyp(typ: PlatzTypE): List<Platz> = transaction {
|
||||
PlaetzeTable.selectAll().where { PlaetzeTable.typ eq typ }
|
||||
.map { rowToPlatz(it) }
|
||||
}
|
||||
|
||||
override suspend fun create(platz: Platz): Platz = transaction {
|
||||
PlaetzeTable.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
|
||||
}
|
||||
platz
|
||||
}
|
||||
|
||||
override suspend fun update(id: Uuid, platz: Platz): Platz? = transaction {
|
||||
val updateCount = PlaetzeTable.update({ PlaetzeTable.id eq id }) {
|
||||
it[turnierId] = platz.turnierId
|
||||
it[name] = platz.name
|
||||
it[dimension] = platz.dimension
|
||||
it[boden] = platz.boden
|
||||
it[typ] = platz.typ
|
||||
}
|
||||
if (updateCount > 0) {
|
||||
PlaetzeTable.selectAll().where { PlaetzeTable.id eq id }
|
||||
.map { rowToPlatz(it) }
|
||||
.singleOrNull()
|
||||
} else null
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
||||
PlaetzeTable.deleteWhere { PlaetzeTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun search(query: String): List<Platz> = transaction {
|
||||
PlaetzeTable.selectAll().where {
|
||||
(PlaetzeTable.name.lowerCase() like "%${query.lowercase()}%") or
|
||||
(PlaetzeTable.dimension?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE) or
|
||||
(PlaetzeTable.boden?.lowerCase()?.like("%${query.lowercase()}%") ?: Op.FALSE)
|
||||
}.map { rowToPlatz(it) }
|
||||
}
|
||||
|
||||
private fun rowToPlatz(row: ResultRow): Platz {
|
||||
return Platz(
|
||||
id = row[PlaetzeTable.id],
|
||||
turnierId = row[PlaetzeTable.turnierId],
|
||||
name = row[PlaetzeTable.name],
|
||||
dimension = row[PlaetzeTable.dimension],
|
||||
boden = row[PlaetzeTable.boden],
|
||||
typ = row[PlaetzeTable.typ]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
package at.mocode.routes
|
||||
|
||||
import at.mocode.repositories.PersonRepository
|
||||
import at.mocode.repositories.PostgresPersonRepository
|
||||
import at.mocode.di.ServiceRegistry
|
||||
import at.mocode.di.resolve
|
||||
import at.mocode.services.PersonService
|
||||
import at.mocode.stammdaten.Person
|
||||
import at.mocode.utils.ResponseUtils.handleException
|
||||
import at.mocode.utils.ResponseUtils.respondCreated
|
||||
import at.mocode.utils.ResponseUtils.respondNoContent
|
||||
import at.mocode.utils.ResponseUtils.respondNotFound
|
||||
import at.mocode.utils.ResponseUtils.respondSuccess
|
||||
import at.mocode.utils.ResponseUtils.respondValidationError
|
||||
import at.mocode.validation.ValidationException
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.plugins.openapi.*
|
||||
@@ -11,86 +19,70 @@ import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.personRoutes() {
|
||||
val personRepository: PersonRepository = PostgresPersonRepository()
|
||||
val personService: PersonService = ServiceRegistry.serviceLocator.resolve()
|
||||
|
||||
route("/persons") {
|
||||
// GET /api/persons - Get all persons
|
||||
get {
|
||||
try {
|
||||
val persons = personRepository.findAll()
|
||||
call.respond(HttpStatusCode.OK, persons)
|
||||
val persons = personService.getAllPersons()
|
||||
call.respondSuccess(persons)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
call.handleException(e, "getting all persons")
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/persons/{id} - Get person by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val id = call.parameters["id"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing person ID")
|
||||
)
|
||||
val id = call.parameters["id"] ?: return@get call.respondValidationError("Missing person ID")
|
||||
val uuid = uuidFrom(id)
|
||||
val person = personRepository.findById(uuid)
|
||||
val person = personService.getPersonById(uuid)
|
||||
if (person != null) {
|
||||
call.respond(HttpStatusCode.OK, person)
|
||||
call.respondSuccess(person)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found"))
|
||||
call.respondNotFound("Person")
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
call.handleException(e, "getting person by ID")
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/persons/oeps/{oepsSatzNr} - Get person by OEPS number
|
||||
get("/oeps/{oepsSatzNr}") {
|
||||
try {
|
||||
val oepsSatzNr = call.parameters["oepsSatzNr"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing OEPS Satz number")
|
||||
)
|
||||
val person = personRepository.findByOepsSatzNr(oepsSatzNr)
|
||||
val oepsSatzNr = call.parameters["oepsSatzNr"] ?: return@get call.respondValidationError("Missing OEPS Satz number")
|
||||
val person = personService.getPersonByOepsSatzNr(oepsSatzNr)
|
||||
if (person != null) {
|
||||
call.respond(HttpStatusCode.OK, person)
|
||||
call.respondSuccess(person)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found"))
|
||||
call.respondNotFound("Person")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
call.handleException(e, "getting person by OEPS number")
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/persons/search?q={query} - Search persons
|
||||
get("/search") {
|
||||
try {
|
||||
val query = call.request.queryParameters["q"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing search query parameter 'q'")
|
||||
)
|
||||
val persons = personRepository.search(query)
|
||||
call.respond(HttpStatusCode.OK, persons)
|
||||
val query = call.request.queryParameters["q"] ?: return@get call.respondValidationError("Missing search query parameter 'q'")
|
||||
val persons = personService.searchPersons(query)
|
||||
call.respondSuccess(persons)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
call.handleException(e, "searching persons")
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/persons/verein/{vereinId} - Get persons by club ID
|
||||
get("/verein/{vereinId}") {
|
||||
try {
|
||||
val vereinId = call.parameters["vereinId"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing verein ID")
|
||||
)
|
||||
val vereinId = call.parameters["vereinId"] ?: return@get call.respondValidationError("Missing verein ID")
|
||||
val uuid = uuidFrom(vereinId)
|
||||
val persons = personRepository.findByVereinId(uuid)
|
||||
call.respond(HttpStatusCode.OK, persons)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
val persons = personService.getPersonsByVereinId(uuid)
|
||||
call.respondSuccess(persons)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
call.handleException(e, "getting persons by verein ID")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,53 +90,53 @@ fun Route.personRoutes() {
|
||||
post {
|
||||
try {
|
||||
val person = call.receive<Person>()
|
||||
val createdPerson = personRepository.create(person)
|
||||
call.respond(HttpStatusCode.Created, createdPerson)
|
||||
val createdPerson = personService.createPerson(person)
|
||||
call.respondCreated(createdPerson)
|
||||
} catch (e: ValidationException) {
|
||||
call.respondValidationError(
|
||||
"Person validation failed",
|
||||
e.validationResult.errors.joinToString("; ") { "${it.field}: ${it.message}" }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
|
||||
call.handleException(e, "creating person")
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/persons/{id} - Update person
|
||||
put("/{id}") {
|
||||
try {
|
||||
val id = call.parameters["id"] ?: return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing person ID")
|
||||
)
|
||||
val id = call.parameters["id"] ?: return@put call.respondValidationError("Missing person ID")
|
||||
val uuid = uuidFrom(id)
|
||||
val person = call.receive<Person>()
|
||||
val updatedPerson = personRepository.update(uuid, person)
|
||||
val updatedPerson = personService.updatePerson(uuid, person)
|
||||
if (updatedPerson != null) {
|
||||
call.respond(HttpStatusCode.OK, updatedPerson)
|
||||
call.respondSuccess(updatedPerson)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found"))
|
||||
call.respondNotFound("Person")
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
} catch (e: ValidationException) {
|
||||
call.respondValidationError(
|
||||
"Person validation failed",
|
||||
e.validationResult.errors.joinToString("; ") { "${it.field}: ${it.message}" }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
|
||||
call.handleException(e, "updating person")
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/persons/{id} - Delete person
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val id = call.parameters["id"] ?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing person ID")
|
||||
)
|
||||
val id = call.parameters["id"] ?: return@delete call.respondValidationError("Missing person ID")
|
||||
val uuid = uuidFrom(id)
|
||||
val deleted = personRepository.delete(uuid)
|
||||
val deleted = personService.deletePerson(uuid)
|
||||
if (deleted) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
call.respondNoContent()
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Person not found"))
|
||||
call.respondNotFound("Person")
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
call.handleException(e, "deleting person")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package at.mocode.routes
|
||||
|
||||
import at.mocode.model.Platz
|
||||
import at.mocode.enums.PlatzTypE
|
||||
import at.mocode.services.ServiceLocator
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.platzRoutes() {
|
||||
val platzService = ServiceLocator.platzService
|
||||
|
||||
route("/plaetze") {
|
||||
// GET /api/plaetze - Get all places
|
||||
get {
|
||||
try {
|
||||
val plaetze = platzService.getAllPlaetze()
|
||||
call.respond(HttpStatusCode.OK, plaetze)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/plaetze/{id} - Get place by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val id = call.parameters["id"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing place ID")
|
||||
)
|
||||
val uuid = uuidFrom(id)
|
||||
val platz = platzService.getPlatzById(uuid)
|
||||
if (platz != null) {
|
||||
call.respond(HttpStatusCode.OK, platz)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Place not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/plaetze/search?q={query} - Search places
|
||||
get("/search") {
|
||||
try {
|
||||
val query = call.request.queryParameters["q"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing search query parameter 'q'")
|
||||
)
|
||||
val plaetze = platzService.searchPlaetze(query)
|
||||
call.respond(HttpStatusCode.OK, plaetze)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/plaetze/turnier/{turnierId} - Get places by tournament ID
|
||||
get("/turnier/{turnierId}") {
|
||||
try {
|
||||
val turnierId = call.parameters["turnierId"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing tournament ID")
|
||||
)
|
||||
val uuid = uuidFrom(turnierId)
|
||||
val plaetze = platzService.getPlaetzeByTurnierId(uuid)
|
||||
call.respond(HttpStatusCode.OK, plaetze)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/plaetze/typ/{typ} - Get places by type
|
||||
get("/typ/{typ}") {
|
||||
try {
|
||||
val typParam = call.parameters["typ"] ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing place type")
|
||||
)
|
||||
val typ = PlatzTypE.valueOf(typParam.uppercase())
|
||||
val plaetze = platzService.getPlaetzeByTyp(typ)
|
||||
call.respond(HttpStatusCode.OK, plaetze)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid place type: ${e.message}"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/plaetze - Create new place
|
||||
post {
|
||||
try {
|
||||
val platz = call.receive<Platz>()
|
||||
val createdPlatz = platzService.createPlatz(platz)
|
||||
call.respond(HttpStatusCode.Created, createdPlatz)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/plaetze/{id} - Update place
|
||||
put("/{id}") {
|
||||
try {
|
||||
val id = call.parameters["id"] ?: return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing place ID")
|
||||
)
|
||||
val uuid = uuidFrom(id)
|
||||
val platz = call.receive<Platz>()
|
||||
val updatedPlatz = platzService.updatePlatz(uuid, platz)
|
||||
if (updatedPlatz != null) {
|
||||
call.respond(HttpStatusCode.OK, updatedPlatz)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Place not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/plaetze/{id} - Delete place
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val id = call.parameters["id"] ?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing place ID")
|
||||
)
|
||||
val uuid = uuidFrom(id)
|
||||
val deleted = platzService.deletePlatz(uuid)
|
||||
if (deleted) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, mapOf("error" to "Place not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Invalid UUID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ object RouteConfiguration {
|
||||
turnierRoutes()
|
||||
bewerbRoutes()
|
||||
abteilungRoutes()
|
||||
|
||||
// Places/Venues for events
|
||||
platzRoutes()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ class BewerbService(private val bewerbRepository: BewerbRepository) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize result list for a competition
|
||||
* Finalize the result list for a competition
|
||||
*/
|
||||
suspend fun finalizeErgebnisliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
@@ -139,7 +139,7 @@ class BewerbService(private val bewerbRepository: BewerbRepository) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen start list for a competition
|
||||
* Reopen the start list for a competition
|
||||
*/
|
||||
suspend fun reopenStartliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
@@ -152,7 +152,7 @@ class BewerbService(private val bewerbRepository: BewerbRepository) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen result list for a competition
|
||||
* Reopen the result list for a competition
|
||||
*/
|
||||
suspend fun reopenErgebnisliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
|
||||
@@ -126,7 +126,7 @@ class DomLizenzService(private val domLizenzRepository: DomLizenzRepository) {
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
// For example, checking if the license type is valid, person exists, etc.
|
||||
// Additional validation rules can be added here,
|
||||
// For example, checking if the license type is valid, a person exists, etc.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package at.mocode.services
|
||||
|
||||
import at.mocode.stammdaten.Person
|
||||
import at.mocode.repositories.PersonRepository
|
||||
import at.mocode.validation.PersonValidator
|
||||
import at.mocode.validation.ValidationException
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
@@ -55,7 +57,8 @@ class PersonService(private val personRepository: PersonRepository) {
|
||||
* Create a new person with business validation
|
||||
*/
|
||||
suspend fun createPerson(person: Person): Person {
|
||||
validatePerson(person)
|
||||
// Use comprehensive validation
|
||||
PersonValidator.validateAndThrow(person)
|
||||
|
||||
// Check if OEPS Satz number already exists
|
||||
person.oepsSatzNr?.let { oepsNr ->
|
||||
@@ -72,7 +75,8 @@ class PersonService(private val personRepository: PersonRepository) {
|
||||
* Update an existing person
|
||||
*/
|
||||
suspend fun updatePerson(id: Uuid, person: Person): Person? {
|
||||
validatePerson(person)
|
||||
// Use comprehensive validation
|
||||
PersonValidator.validateAndThrow(person)
|
||||
|
||||
// Check if OEPS Satz number conflicts with another person
|
||||
person.oepsSatzNr?.let { oepsNr ->
|
||||
@@ -92,31 +96,4 @@ class PersonService(private val personRepository: PersonRepository) {
|
||||
return personRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate person data according to business rules
|
||||
*/
|
||||
private fun validatePerson(person: Person) {
|
||||
if (person.vorname.isBlank()) {
|
||||
throw IllegalArgumentException("Person first name cannot be blank")
|
||||
}
|
||||
|
||||
if (person.nachname.isBlank()) {
|
||||
throw IllegalArgumentException("Person last name cannot be blank")
|
||||
}
|
||||
|
||||
if (person.vorname.length > 100) {
|
||||
throw IllegalArgumentException("Person first name cannot exceed 100 characters")
|
||||
}
|
||||
|
||||
if (person.nachname.length > 100) {
|
||||
throw IllegalArgumentException("Person last name cannot exceed 100 characters")
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
person.oepsSatzNr?.let { oepsNr ->
|
||||
if (oepsNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Satz number cannot be blank if provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.Platz
|
||||
import at.mocode.repositories.PlatzRepository
|
||||
import at.mocode.enums.PlatzTypE
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Platz (Place) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class PlatzService(private val platzRepository: PlatzRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all places
|
||||
*/
|
||||
suspend fun getAllPlaetze(): List<Platz> {
|
||||
return platzRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a place by its unique identifier
|
||||
*/
|
||||
suspend fun getPlatzById(id: Uuid): Platz? {
|
||||
return platzRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find places by tournament ID
|
||||
*/
|
||||
suspend fun getPlaetzeByTurnierId(turnierId: Uuid): List<Platz> {
|
||||
return platzRepository.findByTurnierId(turnierId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find places by type
|
||||
*/
|
||||
suspend fun getPlaetzeByTyp(typ: PlatzTypE): List<Platz> {
|
||||
return platzRepository.findByTyp(typ)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for places by query string
|
||||
*/
|
||||
suspend fun searchPlaetze(query: String): List<Platz> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return platzRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new place with business validation
|
||||
*/
|
||||
suspend fun createPlatz(platz: Platz): Platz {
|
||||
validatePlatz(platz)
|
||||
return platzRepository.create(platz)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing place
|
||||
*/
|
||||
suspend fun updatePlatz(id: Uuid, platz: Platz): Platz? {
|
||||
validatePlatz(platz)
|
||||
return platzRepository.update(id, platz)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a place by ID
|
||||
*/
|
||||
suspend fun deletePlatz(id: Uuid): Boolean {
|
||||
return platzRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate place data according to business rules
|
||||
*/
|
||||
private fun validatePlatz(platz: Platz) {
|
||||
if (platz.name.isBlank()) {
|
||||
throw IllegalArgumentException("Place name cannot be blank")
|
||||
}
|
||||
|
||||
if (platz.name.length > 100) {
|
||||
throw IllegalArgumentException("Place name cannot exceed 100 characters")
|
||||
}
|
||||
|
||||
platz.dimension?.let { dimension ->
|
||||
if (dimension.length > 50) {
|
||||
throw IllegalArgumentException("Place dimension cannot exceed 50 characters")
|
||||
}
|
||||
}
|
||||
|
||||
platz.boden?.let { boden ->
|
||||
if (boden.length > 100) {
|
||||
throw IllegalArgumentException("Place boden cannot exceed 100 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ object ServiceLocator {
|
||||
|
||||
// Repository instances - lazy initialization
|
||||
val artikelRepository: ArtikelRepository by lazy { PostgresArtikelRepository() }
|
||||
val platzRepository: PlatzRepository by lazy { PostgresPlatzRepository() }
|
||||
val vereinRepository: VereinRepository by lazy { PostgresVereinRepository() }
|
||||
val personRepository: PersonRepository by lazy { PostgresPersonRepository() }
|
||||
val domLizenzRepository: DomLizenzRepository by lazy { PostgresDomLizenzRepository() }
|
||||
@@ -23,6 +24,7 @@ object ServiceLocator {
|
||||
|
||||
// Service instances - lazy initialization with dependency injection
|
||||
val artikelService: ArtikelService by lazy { ArtikelService(artikelRepository) }
|
||||
val platzService: PlatzService by lazy { PlatzService(platzRepository) }
|
||||
val vereinService: VereinService by lazy { VereinService(vereinRepository) }
|
||||
val personService: PersonService by lazy { PersonService(personRepository) }
|
||||
val domLizenzService: DomLizenzService by lazy { DomLizenzService(domLizenzRepository) }
|
||||
@@ -39,6 +41,7 @@ object ServiceLocator {
|
||||
fun initializeAll() {
|
||||
// Initialize repositories
|
||||
artikelRepository
|
||||
platzRepository
|
||||
vereinRepository
|
||||
personRepository
|
||||
domLizenzRepository
|
||||
@@ -51,6 +54,7 @@ object ServiceLocator {
|
||||
|
||||
// Initialize services
|
||||
artikelService
|
||||
platzService
|
||||
vereinService
|
||||
personService
|
||||
domLizenzService
|
||||
|
||||
@@ -74,7 +74,7 @@ class TurnierService(private val turnierRepository: TurnierRepository) {
|
||||
suspend fun updateTurnier(id: Uuid, turnier: Turnier): Turnier? {
|
||||
validateTurnier(turnier)
|
||||
|
||||
// Check if OEPS tournament number conflicts with another tournament
|
||||
// Check if the OEPS tournament number conflicts with another tournament
|
||||
turnier.oepsTurnierNr?.let { oepsNr ->
|
||||
val existing = turnierRepository.findByOepsTurnierNr(oepsNr)
|
||||
if (existing != null && existing.id != id) {
|
||||
|
||||
@@ -61,7 +61,7 @@ class VereinService(private val vereinRepository: VereinRepository) {
|
||||
validateVerein(verein)
|
||||
|
||||
// Check if OEPS number already exists
|
||||
verein.oepsVereinsNr?.let { oepsNr ->
|
||||
verein.oepsVereinsNr.let { oepsNr ->
|
||||
val existing = vereinRepository.findByOepsVereinsNr(oepsNr)
|
||||
if (existing != null) {
|
||||
throw IllegalArgumentException("A club with OEPS number '$oepsNr' already exists")
|
||||
@@ -77,8 +77,8 @@ class VereinService(private val vereinRepository: VereinRepository) {
|
||||
suspend fun updateVerein(id: Uuid, verein: Verein): Verein? {
|
||||
validateVerein(verein)
|
||||
|
||||
// Check if OEPS number conflicts with another club
|
||||
verein.oepsVereinsNr?.let { oepsNr ->
|
||||
// Check if the OEPS number conflicts with another club
|
||||
verein.oepsVereinsNr.let { oepsNr ->
|
||||
val existing = vereinRepository.findByOepsVereinsNr(oepsNr)
|
||||
if (existing != null && existing.id != id) {
|
||||
throw IllegalArgumentException("A club with OEPS number '$oepsNr' already exists")
|
||||
@@ -108,7 +108,7 @@ class VereinService(private val vereinRepository: VereinRepository) {
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
verein.oepsVereinsNr?.let { oepsNr ->
|
||||
verein.oepsVereinsNr.let { oepsNr ->
|
||||
if (oepsNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Vereins number cannot be blank if provided")
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
package at.mocode.tables.veranstaltung
|
||||
|
||||
import at.mocode.tables.AbteilungTable
|
||||
import at.mocode.tables.PlaetzeTable
|
||||
import at.mocode.tables.TurniereTable
|
||||
import at.mocode.tables.VeranstaltungenTable
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
|
||||
// Event models tables
|
||||
object PruefungAbteilungTable : Table("pruefung_abteilungen") {
|
||||
val id = uuid("id")
|
||||
val pruefungId = uuid("pruefung_id") // FK to Pruefung when implemented
|
||||
val abteilungId = uuid("abteilung_id").references(AbteilungTable.id)
|
||||
val reihenfolge = integer("reihenfolge").default(1)
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, pruefungId)
|
||||
index(false, abteilungId)
|
||||
index(false, reihenfolge)
|
||||
}
|
||||
}
|
||||
|
||||
object PruefungOEPSTable : Table("pruefung_oeps") {
|
||||
val id = uuid("id")
|
||||
val pruefungId = uuid("pruefung_id") // FK to Pruefung when implemented
|
||||
val oepsCode = varchar("oeps_code", 50)
|
||||
val oepsBezeichnung = varchar("oeps_bezeichnung", 255)
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, pruefungId)
|
||||
index(false, oepsCode)
|
||||
}
|
||||
}
|
||||
|
||||
object TurnierHatPlatzTable : Table("turnier_hat_platz") {
|
||||
val id = uuid("id")
|
||||
val turnierId = uuid("turnier_id").references(TurniereTable.id)
|
||||
val platzId = uuid("platz_id").references(PlaetzeTable.id)
|
||||
val istHauptplatz = bool("ist_hauptplatz").default(false)
|
||||
val verfuegbarVon = date("verfuegbar_von").nullable()
|
||||
val verfuegbarBis = date("verfuegbar_bis").nullable()
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, turnierId)
|
||||
index(false, platzId)
|
||||
index(false, istHauptplatz)
|
||||
}
|
||||
}
|
||||
|
||||
object TurnierOEPSTable : Table("turnier_oeps") {
|
||||
val id = uuid("id")
|
||||
val turnierId = uuid("turnier_id").references(TurniereTable.id)
|
||||
val oepsTurnierNr = varchar("oeps_turnier_nr", 50)
|
||||
val oepsKategorie = varchar("oeps_kategorie", 100)
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, turnierId)
|
||||
index(false, oepsTurnierNr)
|
||||
index(false, oepsKategorie)
|
||||
}
|
||||
}
|
||||
|
||||
object VeranstaltungsRahmenTable : Table("veranstaltungs_rahmen") {
|
||||
val id = uuid("id")
|
||||
val veranstaltungId = uuid("veranstaltung_id").references(VeranstaltungenTable.id)
|
||||
val rahmenTyp = varchar("rahmen_typ", 100)
|
||||
val bezeichnung = varchar("bezeichnung", 255)
|
||||
val beschreibung = text("beschreibung").nullable()
|
||||
val reihenfolge = integer("reihenfolge").default(1)
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, veranstaltungId)
|
||||
index(false, rahmenTyp)
|
||||
index(false, reihenfolge)
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package at.mocode.tables.veranstaltung
|
||||
|
||||
import at.mocode.tables.domaene.DomPersonTable
|
||||
import at.mocode.tables.domaene.DomVereinTable
|
||||
import at.mocode.tables.oeto_verwaltung.SportlicheStammdatenTable
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.time
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Database tables for the complete event management hierarchy:
|
||||
* VeranstaltungsRahmen -> Turnier_OEPS -> Pruefung_OEPS -> Pruefung_Abteilung
|
||||
*/
|
||||
|
||||
// Top level: Veranstaltungen (Events)
|
||||
object VeranstaltungsRahmenTable : Table("veranstaltungs_rahmen") {
|
||||
val veranstRahmenId = uuid("veranst_rahmen_id")
|
||||
val name = varchar("name", 255)
|
||||
val eventTypIntern = varchar("event_typ_intern", 100).nullable()
|
||||
val ortName = varchar("ort_name", 255)
|
||||
val ortStrasse = varchar("ort_strasse", 255).nullable()
|
||||
val ortPlz = varchar("ort_plz", 10).nullable()
|
||||
val ortOrt = varchar("ort_ort", 100).nullable()
|
||||
val datumVonGesamt = date("datum_von_gesamt")
|
||||
val datumBisGesamt = date("datum_bis_gesamt")
|
||||
val logoUrl = varchar("logo_url", 500).nullable()
|
||||
val webseiteUrl = varchar("webseite_url", 500).nullable()
|
||||
val hauptveranstalterDomVereinId = uuid("hauptveranstalter_dom_verein_id").nullable().references(DomVereinTable.vereinId)
|
||||
val hauptKontaktpersonDomPersonId = uuid("haupt_kontaktperson_dom_person_id").nullable().references(DomPersonTable.personId)
|
||||
val status = varchar("status", 50).default("IN_PLANUNG")
|
||||
val anmerkungenAllgemein = text("anmerkungen_allgemein").nullable()
|
||||
val berichtAnmerkungSanitaer = text("bericht_anmerkung_sanitaer").nullable()
|
||||
val berichtAnmerkungParkenEntladen = text("bericht_anmerkung_parken_entladen").nullable()
|
||||
val berichtAnmerkungSponsorenBetreuung = text("bericht_anmerkung_sponsoren_betreuung").nullable()
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(veranstRahmenId)
|
||||
|
||||
init {
|
||||
index(false, name)
|
||||
index(false, datumVonGesamt)
|
||||
index(false, status)
|
||||
}
|
||||
}
|
||||
|
||||
// Second level: Turniere (Tournaments)
|
||||
object TurnierOEPSTable : Table("turnier_oeps") {
|
||||
val turnierOepsId = uuid("turnier_oeps_id")
|
||||
val veranstaltungsRahmenId = uuid("veranstaltungs_rahmen_id").references(VeranstaltungsRahmenTable.veranstRahmenId)
|
||||
val oepsTurnierNr = varchar("oeps_turnier_nr", 50)
|
||||
val titel = varchar("titel", 500)
|
||||
val untertitel = varchar("untertitel", 500).nullable()
|
||||
val hauptsparte = varchar("hauptsparte", 50)
|
||||
val regelwerkTyp = varchar("regelwerk_typ", 50).default("OETO")
|
||||
val datumVon = date("datum_von")
|
||||
val datumBis = date("datum_bis")
|
||||
val nennschlussOffiziell = datetime("nennschluss_offiziell").nullable()
|
||||
val pdfAusschreibungUrl = varchar("pdf_ausschreibung_url", 500).nullable()
|
||||
val kommentarIntern = text("kommentar_intern").nullable()
|
||||
val typNationalInternational = varchar("typ_national_international", 50).default("National")
|
||||
val spracheDefault = varchar("sprache_default", 50).default("Deutsch")
|
||||
val logoTurnierUrl = varchar("logo_turnier_url", 500).nullable()
|
||||
val turnierleiterDomPersonId = uuid("turnierleiter_dom_person_id").nullable().references(DomPersonTable.personId)
|
||||
val turnierbeauftragterDomPersonId = uuid("turnierbeauftragter_dom_person_id").nullable().references(DomPersonTable.personId)
|
||||
val meldestelleTelefon = varchar("meldestelle_telefon", 50).nullable()
|
||||
val meldestelleOeffnungszeiten = varchar("meldestelle_oeffnungszeiten", 255).nullable()
|
||||
val startUndErgebnislistenUrl = varchar("start_und_ergebnislisten_url", 500).nullable()
|
||||
val statusTurnier = varchar("status_turnier", 50).default("IN_PLANUNG")
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(turnierOepsId)
|
||||
|
||||
init {
|
||||
index(false, veranstaltungsRahmenId)
|
||||
index(false, oepsTurnierNr)
|
||||
index(false, hauptsparte)
|
||||
index(false, datumVon)
|
||||
}
|
||||
}
|
||||
|
||||
// Junction table for tournament categories
|
||||
object TurnierOEPSKategorienTable : Table("turnier_oeps_kategorien") {
|
||||
val id = uuid("id")
|
||||
val turnierOepsId = uuid("turnier_oeps_id").references(TurnierOEPSTable.turnierOepsId)
|
||||
val oetoKategorieStammdatumId = uuid("oeto_kategorie_stammdatum_id").references(SportlicheStammdatenTable.stammdatumId)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, turnierOepsId)
|
||||
index(false, oetoKategorieStammdatumId)
|
||||
}
|
||||
}
|
||||
|
||||
// Third level: Bewerbe (Competitions)
|
||||
object PruefungOEPSTable : Table("pruefung_oeps") {
|
||||
val pruefungDbId = uuid("pruefung_db_id")
|
||||
val turnierOepsId = uuid("turnier_oeps_id").references(TurnierOEPSTable.turnierOepsId)
|
||||
val oepsBewerbNrAnzeige = integer("oeps_bewerb_nr_anzeige")
|
||||
val nameTextUebergeordnet = varchar("name_text_uebergeordnet", 500)
|
||||
val sparte = varchar("sparte", 50)
|
||||
val oepsKategorieStammdatumId = uuid("oeps_kategorie_stammdatum_id").references(SportfachlicheStammdatenTable.stammdatumId)
|
||||
val istDotiert = bool("ist_dotiert").default(false)
|
||||
val startgeldStandard = decimal("startgeld_standard", 10, 2).nullable()
|
||||
val oepsBewerbsartCodeZns = varchar("oeps_bewerbsart_code_zns", 50).nullable()
|
||||
val notizenIntern = text("notizen_intern").nullable()
|
||||
val istAbgesagt = bool("ist_abgesagt").default(false)
|
||||
val erfordertAbteilungsAuswahlFuerNennung = bool("erfordert_abteilungs_auswahl_fuer_nennung").default(true)
|
||||
val standardPlatzId = uuid("standard_platz_id").nullable()
|
||||
val standardDatum = date("standard_datum").nullable()
|
||||
val standardBeginnzeitTyp = varchar("standard_beginnzeit_typ", 50).default("ANSCHLIESSEND")
|
||||
val standardBeginnzeitFix = time("standard_beginnzeit_fix").nullable()
|
||||
val standardBeginnNachPruefungId = uuid("standard_beginn_nach_pruefung_id").nullable().references(pruefungDbId)
|
||||
val standardBeginnzeitCa = time("standard_beginnzeit_ca").nullable()
|
||||
val anzahlAbteilungen = integer("anzahl_abteilungen").default(0)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(pruefungDbId)
|
||||
|
||||
init {
|
||||
index(false, turnierOepsId)
|
||||
index(false, oepsBewerbNrAnzeige)
|
||||
index(false, sparte)
|
||||
index(false, standardDatum)
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth level: Abteilungen (Divisions/Classes)
|
||||
object PruefungAbteilungTable : Table("pruefung_abteilung") {
|
||||
val pruefungAbteilungDbId = uuid("pruefung_abteilung_db_id")
|
||||
val pruefungDbId = uuid("pruefung_db_id").references(PruefungOEPSTable.pruefungDbId)
|
||||
val abteilungsKennzeichen = varchar("abteilungs_kennzeichen", 50)
|
||||
val bezeichnungOeffentlich = varchar("bezeichnung_oeffentlich", 500).nullable()
|
||||
val bezeichnungIntern = varchar("bezeichnung_intern", 500).nullable()
|
||||
val teilKritMinLizenzStammdatumId = uuid("teil_krit_min_lizenz_stammdatum_id").nullable().references(SportfachlicheStammdatenTable.stammdatumId)
|
||||
val teilKritMaxLizenzStammdatumId = uuid("teil_krit_max_lizenz_stammdatum_id").nullable().references(SportfachlicheStammdatenTable.stammdatumId)
|
||||
val teilKritMinPferdealter = integer("teil_krit_min_pferdealter").nullable()
|
||||
val teilKritMaxPferdealter = integer("teil_krit_max_pferdealter").nullable()
|
||||
val teilKritAltersklasseReiterStammdatumId = uuid("teil_krit_altersklasse_reiter_stammdatum_id").nullable().references(SportfachlicheStammdatenTable.stammdatumId)
|
||||
val teilKritPferderasseStammdatumId = uuid("teil_krit_pferderasse_stammdatum_id").nullable().references(SportfachlicheStammdatenTable.stammdatumId)
|
||||
val teilKritAnzahlStarterMin = integer("teil_krit_anzahl_starter_min").nullable()
|
||||
val teilKritAnzahlStarterMax = integer("teil_krit_anzahl_starter_max").nullable()
|
||||
val teilKritFreiTextBeschreibung = text("teil_krit_frei_text_beschreibung").nullable()
|
||||
val startgeld = decimal("startgeld", 10, 2).nullable()
|
||||
val platzId = uuid("platz_id").nullable()
|
||||
val datum = date("datum").nullable()
|
||||
val beginnzeitTyp = varchar("beginnzeit_typ", 50).default("ANSCHLIESSEND")
|
||||
val beginnzeitFix = time("beginnzeit_fix").nullable()
|
||||
val beginnNachAbteilungOderPruefungId = uuid("beginn_nach_abteilung_oder_pruefung_id").nullable()
|
||||
val beginnzeitCa = time("beginnzeit_ca").nullable()
|
||||
val dauerProStartGeschaetztSek = integer("dauer_pro_start_geschaetzt_sek").nullable()
|
||||
val umbauzeitNachAbteilungMin = integer("umbauzeit_nach_abteilung_min").nullable()
|
||||
val besichtigungszeitVorAbteilungMin = integer("besichtigungszeit_vor_abteilung_min").nullable()
|
||||
val stechzeitZusaetzlichMin = integer("stechzeit_zusaetzlich_min").nullable()
|
||||
val istAktivFuerNennung = bool("ist_aktiv_fuer_nennung").default(true)
|
||||
val istStartlisteFinal = bool("ist_startliste_final").default(false)
|
||||
val istErgebnislisteFinal = bool("ist_ergebnisliste_final").default(false)
|
||||
val anzahlNennungen = integer("anzahl_nennungen").default(0)
|
||||
val anzahlStarterEffektiv = integer("anzahl_starter_effektiv").default(0)
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(pruefungAbteilungDbId)
|
||||
|
||||
init {
|
||||
index(false, pruefungDbId)
|
||||
index(false, abteilungsKennzeichen)
|
||||
index(false, datum)
|
||||
index(false, istAktivFuerNennung)
|
||||
}
|
||||
}
|
||||
|
||||
// Junction table for allowed licenses in divisions
|
||||
object PruefungAbteilungErlaubteLizenzenTable : Table("pruefung_abteilung_erlaubte_lizenzen") {
|
||||
val id = uuid("id")
|
||||
val pruefungAbteilungDbId = uuid("pruefung_abteilung_db_id").references(PruefungAbteilungTable.pruefungAbteilungDbId)
|
||||
val lizenzStammdatumId = uuid("lizenz_stammdatum_id").references(SportfachlicheStammdatenTable.stammdatumId)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, pruefungAbteilungDbId)
|
||||
index(false, lizenzStammdatumId)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Meldestelle Server Configuration
|
||||
ktor:
|
||||
deployment:
|
||||
# Server port configuration
|
||||
# Server port configuration - can be overridden with SERVER_PORT environment variable
|
||||
port: 8080
|
||||
# Connection timeout in seconds
|
||||
connectionTimeout: 30
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package at.mocode
|
||||
|
||||
import at.mocode.model.domaene.*
|
||||
import at.mocode.validation.*
|
||||
import at.mocode.enums.DatenQuelleE
|
||||
import at.mocode.enums.PferdeGeschlechtE
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StammdatenValidatorTest {
|
||||
|
||||
@Test
|
||||
fun testDomVereinValidator() {
|
||||
// Valid club
|
||||
val validClub = DomVerein(
|
||||
oepsVereinsNr = "1234",
|
||||
name = "Test Reitverein",
|
||||
kuerzel = "TRV",
|
||||
landId = uuid4(),
|
||||
emailAllgemein = "test@example.com"
|
||||
)
|
||||
|
||||
val result = DomVereinValidator.validate(validClub)
|
||||
if (result.isInvalid()) {
|
||||
println("[DEBUG_LOG] DomVerein validation errors: ${(result as ValidationResult.Invalid).errors}")
|
||||
}
|
||||
assertTrue(DomVereinValidator.isValid(validClub))
|
||||
|
||||
// Invalid club - empty name
|
||||
val invalidClub = DomVerein(
|
||||
oepsVereinsNr = "1234",
|
||||
name = "",
|
||||
landId = uuid4()
|
||||
)
|
||||
|
||||
assertFalse(DomVereinValidator.isValid(invalidClub))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDomPferdValidator() {
|
||||
// Valid horse
|
||||
val validHorse = DomPferd(
|
||||
name = "Test Pferd",
|
||||
oepsSatzNrPferd = "1234567890",
|
||||
oepsKopfNr = "1234",
|
||||
geburtsjahr = 2015,
|
||||
geschlecht = PferdeGeschlechtE.STUTE
|
||||
)
|
||||
|
||||
assertTrue(DomPferdValidator.isValid(validHorse))
|
||||
|
||||
// Invalid horse - empty name
|
||||
val invalidHorse = DomPferd(
|
||||
name = "",
|
||||
oepsSatzNrPferd = "1234567890",
|
||||
oepsKopfNr = "1234"
|
||||
)
|
||||
|
||||
assertFalse(DomPferdValidator.isValid(invalidHorse))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDomLizenzValidator() {
|
||||
// Valid license
|
||||
val validLicense = DomLizenz(
|
||||
personId = uuid4(),
|
||||
lizenzTypGlobalId = uuid4(),
|
||||
gueltigBisJahr = 2024,
|
||||
istAktivBezahltOeps = true
|
||||
)
|
||||
|
||||
assertTrue(DomLizenzValidator.isValid(validLicense))
|
||||
|
||||
// Test expiry check
|
||||
val expiredLicense = DomLizenz(
|
||||
personId = uuid4(),
|
||||
lizenzTypGlobalId = uuid4(),
|
||||
gueltigBisJahr = 2020,
|
||||
istAktivBezahltOeps = true
|
||||
)
|
||||
|
||||
assertTrue(DomLizenzValidator.isLicenseExpired(expiredLicense))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDomQualifikationValidator() {
|
||||
// Valid qualification
|
||||
val validQualification = DomQualifikation(
|
||||
personId = uuid4(),
|
||||
qualTypId = uuid4(),
|
||||
gueltigVon = LocalDate(2020, 1, 1),
|
||||
gueltigBis = LocalDate(2025, 12, 31),
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
val qualResult = DomQualifikationValidator.validate(validQualification)
|
||||
if (qualResult.isInvalid()) {
|
||||
println("[DEBUG_LOG] DomQualifikation validation errors: ${(qualResult as ValidationResult.Invalid).errors}")
|
||||
}
|
||||
assertTrue(DomQualifikationValidator.isValid(validQualification))
|
||||
assertTrue(DomQualifikationValidator.isCurrentlyValid(validQualification))
|
||||
|
||||
// Invalid qualification - end before start
|
||||
val invalidQualification = DomQualifikation(
|
||||
personId = uuid4(),
|
||||
qualTypId = uuid4(),
|
||||
gueltigVon = LocalDate(2025, 1, 1),
|
||||
gueltigBis = LocalDate(2020, 12, 31),
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
assertFalse(DomQualifikationValidator.isValid(invalidQualification))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user