(fix) Implementiere einen Service-Layer
Erstellung von DTOs für alle Ressourcen Implement a versioning system
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package at.mocode.plugins
|
||||
|
||||
import at.mocode.dto.base.VersionManager
|
||||
import at.mocode.dto.base.VersionValidationResult
|
||||
import at.mocode.dto.base.VersionedDto
|
||||
import at.mocode.dto.base.VersionedResponse
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.util.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Plugin for handling API versioning
|
||||
*/
|
||||
val VersioningPlugin = createApplicationPlugin(name = "VersioningPlugin") {
|
||||
|
||||
onCall { call ->
|
||||
// Extract version from headers
|
||||
val clientVersion = call.request.header("API-Version")
|
||||
?: call.request.header("X-API-Version")
|
||||
?: VersionManager.CURRENT_API_VERSION
|
||||
|
||||
// Validate version
|
||||
when (val result = VersionManager.validateClientVersion(clientVersion)) {
|
||||
is VersionValidationResult.Valid -> {
|
||||
call.attributes.put(ClientVersionKey, result.version)
|
||||
}
|
||||
is VersionValidationResult.DeprecatedVersion -> {
|
||||
call.attributes.put(ClientVersionKey, result.version)
|
||||
call.response.header("X-API-Version-Warning", "Version ${result.version} is deprecated")
|
||||
}
|
||||
is VersionValidationResult.UnsupportedVersion -> {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf(
|
||||
"error" to "Unsupported API version: ${result.version}",
|
||||
"supportedVersions" to VersionManager.SUPPORTED_VERSIONS,
|
||||
"currentVersion" to VersionManager.CURRENT_API_VERSION
|
||||
)
|
||||
)
|
||||
return@onCall
|
||||
}
|
||||
is VersionValidationResult.MissingVersion -> {
|
||||
call.attributes.put(ClientVersionKey, VersionManager.CURRENT_API_VERSION)
|
||||
}
|
||||
}
|
||||
|
||||
// Add version info to response headers
|
||||
call.response.header("API-Version", VersionManager.CURRENT_API_VERSION)
|
||||
call.response.header("X-Supported-Versions", VersionManager.SUPPORTED_VERSIONS.joinToString(","))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key for storing client version in call attributes
|
||||
*/
|
||||
val ClientVersionKey = AttributeKey<String>("ClientVersion")
|
||||
|
||||
/**
|
||||
* Extension function to get client version from call
|
||||
*/
|
||||
fun ApplicationCall.getClientVersion(): String {
|
||||
return attributes.getOrNull(ClientVersionKey) ?: VersionManager.CURRENT_API_VERSION
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to respond with versioned data
|
||||
*/
|
||||
suspend inline fun <reified T : VersionedDto> ApplicationCall.respondVersioned(
|
||||
status: HttpStatusCode = HttpStatusCode.OK,
|
||||
data: T
|
||||
) {
|
||||
val versionedResponse = VersionedResponse(
|
||||
data = data,
|
||||
version = VersionManager.getVersionInfo(),
|
||||
timestamp = Clock.System.now().toString()
|
||||
)
|
||||
respond(status, versionedResponse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to respond with versioned list data
|
||||
*/
|
||||
suspend inline fun <reified T : VersionedDto> ApplicationCall.respondVersionedList(
|
||||
status: HttpStatusCode = HttpStatusCode.OK,
|
||||
data: List<T>
|
||||
) {
|
||||
val response = mapOf(
|
||||
"items" to data,
|
||||
"count" to data.size,
|
||||
"version" to VersionManager.getVersionInfo(),
|
||||
"timestamp" to Clock.System.now().toString()
|
||||
)
|
||||
respond(status, response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure versioning for the application
|
||||
*/
|
||||
fun Application.configureVersioning() {
|
||||
install(VersioningPlugin)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package at.mocode.routes
|
||||
|
||||
import at.mocode.repositories.PostgresVereinRepository
|
||||
import at.mocode.repositories.VereinRepository
|
||||
import at.mocode.services.ServiceLocator
|
||||
import at.mocode.stammdaten.Verein
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import io.ktor.http.*
|
||||
@@ -10,13 +9,13 @@ import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
fun Route.vereinRoutes() {
|
||||
val vereinRepository: VereinRepository = PostgresVereinRepository()
|
||||
val vereinService = ServiceLocator.vereinService
|
||||
|
||||
route("/api/vereine") {
|
||||
// GET /api/vereine - Get all clubs
|
||||
get {
|
||||
try {
|
||||
val vereine = vereinRepository.findAll()
|
||||
val vereine = vereinService.getAllVereine()
|
||||
call.respond(HttpStatusCode.OK, vereine)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
@@ -31,7 +30,7 @@ fun Route.vereinRoutes() {
|
||||
mapOf("error" to "Missing verein ID")
|
||||
)
|
||||
val uuid = uuidFrom(id)
|
||||
val verein = vereinRepository.findById(uuid)
|
||||
val verein = vereinService.getVereinById(uuid)
|
||||
if (verein != null) {
|
||||
call.respond(HttpStatusCode.OK, verein)
|
||||
} else {
|
||||
@@ -51,7 +50,7 @@ fun Route.vereinRoutes() {
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing OEPS Vereins number")
|
||||
)
|
||||
val verein = vereinRepository.findByOepsVereinsNr(oepsVereinsNr)
|
||||
val verein = vereinService.getVereinByOepsNr(oepsVereinsNr)
|
||||
if (verein != null) {
|
||||
call.respond(HttpStatusCode.OK, verein)
|
||||
} else {
|
||||
@@ -69,7 +68,7 @@ fun Route.vereinRoutes() {
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing search query parameter 'q'")
|
||||
)
|
||||
val vereine = vereinRepository.search(query)
|
||||
val vereine = vereinService.searchVereine(query)
|
||||
call.respond(HttpStatusCode.OK, vereine)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
@@ -83,7 +82,7 @@ fun Route.vereinRoutes() {
|
||||
HttpStatusCode.BadRequest,
|
||||
mapOf("error" to "Missing bundesland")
|
||||
)
|
||||
val vereine = vereinRepository.findByBundesland(bundesland)
|
||||
val vereine = vereinService.getVereineByBundesland(bundesland)
|
||||
call.respond(HttpStatusCode.OK, vereine)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
|
||||
@@ -94,7 +93,7 @@ fun Route.vereinRoutes() {
|
||||
post {
|
||||
try {
|
||||
val verein = call.receive<Verein>()
|
||||
val createdVerein = vereinRepository.create(verein)
|
||||
val createdVerein = vereinService.createVerein(verein)
|
||||
call.respond(HttpStatusCode.Created, createdVerein)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
|
||||
@@ -110,7 +109,7 @@ fun Route.vereinRoutes() {
|
||||
)
|
||||
val uuid = uuidFrom(id)
|
||||
val verein = call.receive<Verein>()
|
||||
val updatedVerein = vereinRepository.update(uuid, verein)
|
||||
val updatedVerein = vereinService.updateVerein(uuid, verein)
|
||||
if (updatedVerein != null) {
|
||||
call.respond(HttpStatusCode.OK, updatedVerein)
|
||||
} else {
|
||||
@@ -131,7 +130,7 @@ fun Route.vereinRoutes() {
|
||||
mapOf("error" to "Missing verein ID")
|
||||
)
|
||||
val uuid = uuidFrom(id)
|
||||
val deleted = vereinRepository.delete(uuid)
|
||||
val deleted = vereinService.deleteVerein(uuid)
|
||||
if (deleted) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.Abteilung
|
||||
import at.mocode.repositories.AbteilungRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Abteilung (Division/Section) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class AbteilungService(private val abteilungRepository: AbteilungRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all divisions
|
||||
*/
|
||||
suspend fun getAllAbteilungen(): List<Abteilung> {
|
||||
return abteilungRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a division by its unique identifier
|
||||
*/
|
||||
suspend fun getAbteilungById(id: Uuid): Abteilung? {
|
||||
return abteilungRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find divisions by competition (Bewerb) ID
|
||||
*/
|
||||
suspend fun getAbteilungenByBewerbId(bewerbId: Uuid): List<Abteilung> {
|
||||
return abteilungRepository.findByBewerbId(bewerbId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find divisions by active status
|
||||
*/
|
||||
suspend fun getAbteilungenByAktiv(istAktiv: Boolean): List<Abteilung> {
|
||||
return abteilungRepository.findByAktiv(istAktiv)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active divisions
|
||||
*/
|
||||
suspend fun getActiveAbteilungen(): List<Abteilung> {
|
||||
return getAbteilungenByAktiv(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inactive divisions
|
||||
*/
|
||||
suspend fun getInactiveAbteilungen(): List<Abteilung> {
|
||||
return getAbteilungenByAktiv(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for divisions by query string
|
||||
*/
|
||||
suspend fun searchAbteilungen(query: String): List<Abteilung> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return abteilungRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new division with business validation
|
||||
*/
|
||||
suspend fun createAbteilung(abteilung: Abteilung): Abteilung {
|
||||
validateAbteilung(abteilung)
|
||||
return abteilungRepository.create(abteilung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing division
|
||||
*/
|
||||
suspend fun updateAbteilung(id: Uuid, abteilung: Abteilung): Abteilung? {
|
||||
validateAbteilung(abteilung)
|
||||
return abteilungRepository.update(id, abteilung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a division by ID
|
||||
*/
|
||||
suspend fun deleteAbteilung(id: Uuid): Boolean {
|
||||
return abteilungRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a division (soft delete)
|
||||
*/
|
||||
suspend fun deactivateAbteilung(id: Uuid): Abteilung? {
|
||||
val abteilung = getAbteilungById(id)
|
||||
return if (abteilung != null) {
|
||||
val updatedAbteilung = abteilung.copy(istAktiv = false)
|
||||
updateAbteilung(id, updatedAbteilung)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a division
|
||||
*/
|
||||
suspend fun activateAbteilung(id: Uuid): Abteilung? {
|
||||
val abteilung = getAbteilungById(id)
|
||||
return if (abteilung != null) {
|
||||
val updatedAbteilung = abteilung.copy(istAktiv = true)
|
||||
updateAbteilung(id, updatedAbteilung)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get divisions for a specific competition ordered by sequence
|
||||
*/
|
||||
suspend fun getAbteilungenForBewerbOrdered(bewerbId: Uuid): List<Abteilung> {
|
||||
val abteilungen = getAbteilungenByBewerbId(bewerbId)
|
||||
// Sort by abteilungsKennzeichen for basic ordering
|
||||
return abteilungen.sortedBy { it.abteilungsKennzeichen }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate division data according to business rules
|
||||
*/
|
||||
private fun validateAbteilung(abteilung: Abteilung) {
|
||||
if (abteilung.abteilungsKennzeichen.isBlank()) {
|
||||
throw IllegalArgumentException("Division identifier (abteilungsKennzeichen) cannot be blank")
|
||||
}
|
||||
|
||||
if (abteilung.abteilungsKennzeichen.length > 50) {
|
||||
throw IllegalArgumentException("Division identifier cannot exceed 50 characters")
|
||||
}
|
||||
|
||||
// Validate participant count constraints
|
||||
if (abteilung.teilungsKriteriumAnzahlMin != null && abteilung.teilungsKriteriumAnzahlMin!! < 0) {
|
||||
throw IllegalArgumentException("Minimum participant count cannot be negative")
|
||||
}
|
||||
|
||||
if (abteilung.teilungsKriteriumAnzahlMax != null && abteilung.teilungsKriteriumAnzahlMax!! < 0) {
|
||||
throw IllegalArgumentException("Maximum participant count cannot be negative")
|
||||
}
|
||||
|
||||
if (abteilung.teilungsKriteriumAnzahlMin != null && abteilung.teilungsKriteriumAnzahlMax != null) {
|
||||
if (abteilung.teilungsKriteriumAnzahlMin!! > abteilung.teilungsKriteriumAnzahlMax!!) {
|
||||
throw IllegalArgumentException("Minimum participant count cannot be greater than maximum")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate timing constraints
|
||||
if (abteilung.dauerProStartGeschaetztSek != null && abteilung.dauerProStartGeschaetztSek!! < 0) {
|
||||
throw IllegalArgumentException("Estimated duration per start cannot be negative")
|
||||
}
|
||||
|
||||
if (abteilung.umbauzeitNachAbteilungMin != null && abteilung.umbauzeitNachAbteilungMin!! < 0) {
|
||||
throw IllegalArgumentException("Setup time after division cannot be negative")
|
||||
}
|
||||
|
||||
if (abteilung.besichtigungszeitVorAbteilungMin != null && abteilung.besichtigungszeitVorAbteilungMin!! < 0) {
|
||||
throw IllegalArgumentException("Inspection time before division cannot be negative")
|
||||
}
|
||||
|
||||
if (abteilung.stechzeitZusaetzlichMin != null && abteilung.stechzeitZusaetzlichMin!! < 0) {
|
||||
throw IllegalArgumentException("Additional jump-off time cannot be negative")
|
||||
}
|
||||
|
||||
if (abteilung.anzahlStarter < 0) {
|
||||
throw IllegalArgumentException("Number of starters cannot be negative")
|
||||
}
|
||||
|
||||
// Validate text field lengths
|
||||
abteilung.bezeichnungIntern?.let { bezeichnung ->
|
||||
if (bezeichnung.length > 255) {
|
||||
throw IllegalArgumentException("Internal designation cannot exceed 255 characters")
|
||||
}
|
||||
}
|
||||
|
||||
abteilung.bezeichnungAufStartliste?.let { bezeichnung ->
|
||||
if (bezeichnung.length > 255) {
|
||||
throw IllegalArgumentException("Start list designation cannot exceed 255 characters")
|
||||
}
|
||||
}
|
||||
|
||||
abteilung.teilungsKriteriumFreiText?.let { freiText ->
|
||||
if (freiText.length > 500) {
|
||||
throw IllegalArgumentException("Free text division criterion cannot exceed 500 characters")
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.Artikel
|
||||
import at.mocode.repositories.ArtikelRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||
|
||||
/**
|
||||
* Service layer for Artikel (Article) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class ArtikelService(private val artikelRepository: ArtikelRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all articles
|
||||
*/
|
||||
suspend fun getAllArtikel(): List<Artikel> {
|
||||
return artikelRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an article by its unique identifier
|
||||
*/
|
||||
suspend fun getArtikelById(id: Uuid): Artikel? {
|
||||
return artikelRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find articles by Verbandsabgabe status
|
||||
*/
|
||||
suspend fun getArtikelByVerbandsabgabe(istVerbandsabgabe: Boolean): List<Artikel> {
|
||||
return artikelRepository.findByVerbandsabgabe(istVerbandsabgabe)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for articles by query string
|
||||
*/
|
||||
suspend fun searchArtikel(query: String): List<Artikel> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return artikelRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new article with business validation
|
||||
*/
|
||||
suspend fun createArtikel(artikel: Artikel): Artikel {
|
||||
validateArtikel(artikel)
|
||||
return artikelRepository.create(artikel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing article
|
||||
*/
|
||||
suspend fun updateArtikel(id: Uuid, artikel: Artikel): Artikel? {
|
||||
validateArtikel(artikel)
|
||||
return artikelRepository.update(id, artikel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an article by ID
|
||||
*/
|
||||
suspend fun deleteArtikel(id: Uuid): Boolean {
|
||||
return artikelRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Verbandsabgabe articles (federation fee articles)
|
||||
*/
|
||||
suspend fun getVerbandsabgabeArtikel(): List<Artikel> {
|
||||
return getArtikelByVerbandsabgabe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all non-Verbandsabgabe articles
|
||||
*/
|
||||
suspend fun getNonVerbandsabgabeArtikel(): List<Artikel> {
|
||||
return getArtikelByVerbandsabgabe(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate article data according to business rules
|
||||
*/
|
||||
private fun validateArtikel(artikel: Artikel) {
|
||||
if (artikel.bezeichnung.isBlank()) {
|
||||
throw IllegalArgumentException("Article bezeichnung cannot be blank")
|
||||
}
|
||||
|
||||
if (artikel.bezeichnung.length > 255) {
|
||||
throw IllegalArgumentException("Article bezeichnung cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
if (artikel.preis < BigDecimal.ZERO) {
|
||||
throw IllegalArgumentException("Article price cannot be negative")
|
||||
}
|
||||
|
||||
if (artikel.einheit.isBlank()) {
|
||||
throw IllegalArgumentException("Article einheit cannot be blank")
|
||||
}
|
||||
|
||||
if (artikel.einheit.length > 50) {
|
||||
throw IllegalArgumentException("Article einheit cannot exceed 50 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.Bewerb
|
||||
import at.mocode.repositories.BewerbRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Bewerb (Competition) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class BewerbService(private val bewerbRepository: BewerbRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all competitions
|
||||
*/
|
||||
suspend fun getAllBewerbe(): List<Bewerb> {
|
||||
return bewerbRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a competition by its unique identifier
|
||||
*/
|
||||
suspend fun getBewerbById(id: Uuid): Bewerb? {
|
||||
return bewerbRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find competitions by tournament ID
|
||||
*/
|
||||
suspend fun getBewerbeByTurnierId(turnierId: Uuid): List<Bewerb> {
|
||||
return bewerbRepository.findByTurnierId(turnierId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find competitions by sport category (Sparte)
|
||||
*/
|
||||
suspend fun getBewerbeBySparte(sparte: String): List<Bewerb> {
|
||||
if (sparte.isBlank()) {
|
||||
throw IllegalArgumentException("Sparte cannot be blank")
|
||||
}
|
||||
return bewerbRepository.findBySparte(sparte.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Find competitions by class
|
||||
*/
|
||||
suspend fun getBewerbeByKlasse(klasse: String): List<Bewerb> {
|
||||
if (klasse.isBlank()) {
|
||||
throw IllegalArgumentException("Klasse cannot be blank")
|
||||
}
|
||||
return bewerbRepository.findByKlasse(klasse.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Find competitions by start list finalization status
|
||||
*/
|
||||
suspend fun getBewerbeByStartlisteFinal(istFinal: Boolean): List<Bewerb> {
|
||||
return bewerbRepository.findByStartlisteFinal(istFinal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find competitions by result list finalization status
|
||||
*/
|
||||
suspend fun getBewerbeByErgebnislisteFinal(istFinal: Boolean): List<Bewerb> {
|
||||
return bewerbRepository.findByErgebnislisteFinal(istFinal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get competitions with finalized start lists
|
||||
*/
|
||||
suspend fun getBewerbeWithFinalStartliste(): List<Bewerb> {
|
||||
return getBewerbeByStartlisteFinal(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get competitions with finalized result lists
|
||||
*/
|
||||
suspend fun getBewerbeWithFinalErgebnisliste(): List<Bewerb> {
|
||||
return getBewerbeByErgebnislisteFinal(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for competitions by query string
|
||||
*/
|
||||
suspend fun searchBewerbe(query: String): List<Bewerb> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return bewerbRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new competition with business validation
|
||||
*/
|
||||
suspend fun createBewerb(bewerb: Bewerb): Bewerb {
|
||||
validateBewerb(bewerb)
|
||||
return bewerbRepository.create(bewerb)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing competition
|
||||
*/
|
||||
suspend fun updateBewerb(id: Uuid, bewerb: Bewerb): Bewerb? {
|
||||
validateBewerb(bewerb)
|
||||
return bewerbRepository.update(id, bewerb)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a competition by ID
|
||||
*/
|
||||
suspend fun deleteBewerb(id: Uuid): Boolean {
|
||||
return bewerbRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize start list for a competition
|
||||
*/
|
||||
suspend fun finalizeStartliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
return if (bewerb != null) {
|
||||
val updatedBewerb = bewerb.copy(istStartlisteFinal = true)
|
||||
updateBewerb(id, updatedBewerb)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize result list for a competition
|
||||
*/
|
||||
suspend fun finalizeErgebnisliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
return if (bewerb != null) {
|
||||
val updatedBewerb = bewerb.copy(istErgebnislisteFinal = true)
|
||||
updateBewerb(id, updatedBewerb)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen start list for a competition
|
||||
*/
|
||||
suspend fun reopenStartliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
return if (bewerb != null) {
|
||||
val updatedBewerb = bewerb.copy(istStartlisteFinal = false)
|
||||
updateBewerb(id, updatedBewerb)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen result list for a competition
|
||||
*/
|
||||
suspend fun reopenErgebnisliste(id: Uuid): Bewerb? {
|
||||
val bewerb = getBewerbById(id)
|
||||
return if (bewerb != null) {
|
||||
val updatedBewerb = bewerb.copy(istErgebnislisteFinal = false)
|
||||
updateBewerb(id, updatedBewerb)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get competitions for a specific tournament ordered by number
|
||||
*/
|
||||
suspend fun getBewerbeForTurnierOrdered(turnierId: Uuid): List<Bewerb> {
|
||||
val bewerbe = getBewerbeByTurnierId(turnierId)
|
||||
return bewerbe.sortedBy { it.nummer }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate competition data according to business rules
|
||||
*/
|
||||
private fun validateBewerb(bewerb: Bewerb) {
|
||||
if (bewerb.nummer.isBlank()) {
|
||||
throw IllegalArgumentException("Competition number cannot be blank")
|
||||
}
|
||||
|
||||
if (bewerb.nummer.length > 50) {
|
||||
throw IllegalArgumentException("Competition number cannot exceed 50 characters")
|
||||
}
|
||||
|
||||
if (bewerb.bezeichnungOffiziell.isBlank()) {
|
||||
throw IllegalArgumentException("Official designation cannot be blank")
|
||||
}
|
||||
|
||||
if (bewerb.bezeichnungOffiziell.length > 255) {
|
||||
throw IllegalArgumentException("Official designation cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
// Validate participant constraints
|
||||
if (bewerb.maxPferdeProReiter != null && bewerb.maxPferdeProReiter!! < 1) {
|
||||
throw IllegalArgumentException("Maximum horses per rider must be at least 1")
|
||||
}
|
||||
|
||||
if (bewerb.anzahlRichterGeplant != null && bewerb.anzahlRichterGeplant!! < 1) {
|
||||
throw IllegalArgumentException("Number of planned judges must be at least 1")
|
||||
}
|
||||
|
||||
// Validate timing constraints
|
||||
if (bewerb.standardDauerProStartGeschaetztSek != null && bewerb.standardDauerProStartGeschaetztSek!! < 0) {
|
||||
throw IllegalArgumentException("Estimated duration per start cannot be negative")
|
||||
}
|
||||
|
||||
if (bewerb.standardUmbauzeitNachBewerbMin != null && bewerb.standardUmbauzeitNachBewerbMin!! < 0) {
|
||||
throw IllegalArgumentException("Setup time after competition cannot be negative")
|
||||
}
|
||||
|
||||
if (bewerb.standardBesichtigungszeitVorBewerbMin != null && bewerb.standardBesichtigungszeitVorBewerbMin!! < 0) {
|
||||
throw IllegalArgumentException("Inspection time before competition cannot be negative")
|
||||
}
|
||||
|
||||
if (bewerb.standardStechzeitZusaetzlichMin != null && bewerb.standardStechzeitZusaetzlichMin!! < 0) {
|
||||
throw IllegalArgumentException("Additional jump-off time cannot be negative")
|
||||
}
|
||||
|
||||
// Validate text field lengths
|
||||
bewerb.internerName?.let { name ->
|
||||
if (name.length > 255) {
|
||||
throw IllegalArgumentException("Internal name cannot exceed 255 characters")
|
||||
}
|
||||
}
|
||||
|
||||
bewerb.klasse?.let { klasse ->
|
||||
if (klasse.length > 100) {
|
||||
throw IllegalArgumentException("Class cannot exceed 100 characters")
|
||||
}
|
||||
}
|
||||
|
||||
bewerb.kategorieOetoDesBewerbs?.let { kategorie ->
|
||||
if (kategorie.length > 100) {
|
||||
throw IllegalArgumentException("ÖTO category cannot exceed 100 characters")
|
||||
}
|
||||
}
|
||||
|
||||
bewerb.teilnahmebedingungenText?.let { text ->
|
||||
if (text.length > 1000) {
|
||||
throw IllegalArgumentException("Participation conditions text cannot exceed 1000 characters")
|
||||
}
|
||||
}
|
||||
|
||||
bewerb.notizenIntern?.let { notizen ->
|
||||
if (notizen.length > 1000) {
|
||||
throw IllegalArgumentException("Internal notes cannot exceed 1000 characters")
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.domaene.DomLizenz
|
||||
import at.mocode.repositories.DomLizenzRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for DomLizenz (Domain License) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class DomLizenzService(private val domLizenzRepository: DomLizenzRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all licenses
|
||||
*/
|
||||
suspend fun getAllLizenzen(): List<DomLizenz> {
|
||||
return domLizenzRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a license by its unique identifier
|
||||
*/
|
||||
suspend fun getLizenzById(id: Uuid): DomLizenz? {
|
||||
return domLizenzRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find licenses by person ID
|
||||
*/
|
||||
suspend fun getLizenzenByPersonId(personId: Uuid): List<DomLizenz> {
|
||||
return domLizenzRepository.findByPersonId(personId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find licenses by license type global ID
|
||||
*/
|
||||
suspend fun getLizenzenByLizenzTypGlobalId(lizenzTypGlobalId: Uuid): List<DomLizenz> {
|
||||
return domLizenzRepository.findByLizenzTypGlobalId(lizenzTypGlobalId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active licenses by person ID
|
||||
*/
|
||||
suspend fun getActiveLizenzenByPersonId(personId: Uuid): List<DomLizenz> {
|
||||
return domLizenzRepository.findActiveByPersonId(personId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find licenses by validity year
|
||||
*/
|
||||
suspend fun getLizenzenByValidityYear(year: Int): List<DomLizenz> {
|
||||
if (year < 1900 || year > 2100) {
|
||||
throw IllegalArgumentException("Year must be between 1900 and 2100")
|
||||
}
|
||||
return domLizenzRepository.findByValidityYear(year)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for licenses by query string
|
||||
*/
|
||||
suspend fun searchLizenzen(query: String): List<DomLizenz> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return domLizenzRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new license with business validation
|
||||
*/
|
||||
suspend fun createLizenz(domLizenz: DomLizenz): DomLizenz {
|
||||
validateLizenz(domLizenz)
|
||||
return domLizenzRepository.create(domLizenz)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing license
|
||||
*/
|
||||
suspend fun updateLizenz(id: Uuid, domLizenz: DomLizenz): DomLizenz? {
|
||||
validateLizenz(domLizenz)
|
||||
return domLizenzRepository.update(id, domLizenz)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a license by ID
|
||||
*/
|
||||
suspend fun deleteLizenz(id: Uuid): Boolean {
|
||||
return domLizenzRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a person has an active license of a specific type
|
||||
*/
|
||||
suspend fun hasActiveLicense(personId: Uuid, lizenzTypGlobalId: Uuid): Boolean {
|
||||
val activeLicenses = getActiveLizenzenByPersonId(personId)
|
||||
return activeLicenses.any { it.lizenzTypGlobalId == lizenzTypGlobalId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current year licenses for a person
|
||||
*/
|
||||
suspend fun getCurrentYearLizenzenByPersonId(personId: Uuid): List<DomLizenz> {
|
||||
val currentYear = java.time.LocalDate.now().year
|
||||
val allPersonLicenses = getLizenzenByPersonId(personId)
|
||||
return allPersonLicenses.filter { license ->
|
||||
license.gueltigBisJahr == currentYear || license.ausgestelltAm?.year == currentYear
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate license data according to business rules
|
||||
*/
|
||||
private fun validateLizenz(domLizenz: DomLizenz) {
|
||||
// Validate that gueltigBisJahr is reasonable if provided
|
||||
domLizenz.gueltigBisJahr?.let { year ->
|
||||
if (year < 1900 || year > 2100) {
|
||||
throw IllegalArgumentException("License validity year must be between 1900 and 2100")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that ausgestelltAm is not in the future if provided
|
||||
domLizenz.ausgestelltAm?.let { date ->
|
||||
val currentYear = java.time.LocalDate.now().year
|
||||
if (date.year > currentYear) {
|
||||
throw IllegalArgumentException("License issue date cannot be in the future")
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
// For example, checking if the license type is valid, person exists, etc.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.domaene.DomPferd
|
||||
import at.mocode.repositories.DomPferdRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for DomPferd (Domain Horse) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class DomPferdService(private val domPferdRepository: DomPferdRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all horses
|
||||
*/
|
||||
suspend fun getAllPferde(): List<DomPferd> {
|
||||
return domPferdRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a horse by its unique identifier
|
||||
*/
|
||||
suspend fun getPferdById(id: Uuid): DomPferd? {
|
||||
return domPferdRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a horse by its OEPS Satz number
|
||||
*/
|
||||
suspend fun getPferdByOepsSatzNr(oepsSatzNr: String): DomPferd? {
|
||||
if (oepsSatzNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Satz number cannot be blank")
|
||||
}
|
||||
return domPferdRepository.findByOepsSatzNr(oepsSatzNr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find horses by name
|
||||
*/
|
||||
suspend fun getPferdeByName(name: String): List<DomPferd> {
|
||||
if (name.isBlank()) {
|
||||
throw IllegalArgumentException("Horse name cannot be blank")
|
||||
}
|
||||
return domPferdRepository.findByName(name.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a horse by its life number (Lebensnummer)
|
||||
*/
|
||||
suspend fun getPferdByLebensnummer(lebensnummer: String): DomPferd? {
|
||||
if (lebensnummer.isBlank()) {
|
||||
throw IllegalArgumentException("Life number cannot be blank")
|
||||
}
|
||||
return domPferdRepository.findByLebensnummer(lebensnummer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find horses by owner ID
|
||||
*/
|
||||
suspend fun getPferdeByBesitzerId(besitzerId: Uuid): List<DomPferd> {
|
||||
return domPferdRepository.findByBesitzerId(besitzerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find horses by responsible person ID
|
||||
*/
|
||||
suspend fun getPferdeByVerantwortlichePersonId(personId: Uuid): List<DomPferd> {
|
||||
return domPferdRepository.findByVerantwortlichePersonId(personId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find horses by home club ID
|
||||
*/
|
||||
suspend fun getPferdeByHeimatVereinId(vereinId: Uuid): List<DomPferd> {
|
||||
return domPferdRepository.findByHeimatVereinId(vereinId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find horses by breed
|
||||
*/
|
||||
suspend fun getPferdeByRasse(rasse: String): List<DomPferd> {
|
||||
if (rasse.isBlank()) {
|
||||
throw IllegalArgumentException("Breed cannot be blank")
|
||||
}
|
||||
return domPferdRepository.findByRasse(rasse.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Find horses by birth year
|
||||
*/
|
||||
suspend fun getPferdeByGeburtsjahr(geburtsjahr: Int): List<DomPferd> {
|
||||
if (geburtsjahr < 1900 || geburtsjahr > java.time.LocalDate.now().year) {
|
||||
throw IllegalArgumentException("Birth year must be between 1900 and current year")
|
||||
}
|
||||
return domPferdRepository.findByGeburtsjahr(geburtsjahr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all active horses
|
||||
*/
|
||||
suspend fun getActivePferde(): List<DomPferd> {
|
||||
return domPferdRepository.findActiveHorses()
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for horses by query string
|
||||
*/
|
||||
suspend fun searchPferde(query: String): List<DomPferd> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return domPferdRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new horse with business validation
|
||||
*/
|
||||
suspend fun createPferd(domPferd: DomPferd): DomPferd {
|
||||
validatePferd(domPferd)
|
||||
|
||||
// Check if OEPS Satz number already exists
|
||||
domPferd.oepsSatzNrPferd?.let { oepsNr ->
|
||||
val existing = domPferdRepository.findByOepsSatzNr(oepsNr)
|
||||
if (existing != null) {
|
||||
throw IllegalArgumentException("A horse with OEPS Satz number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if life number already exists
|
||||
domPferd.lebensnummer?.let { lebensnummer ->
|
||||
val existing = domPferdRepository.findByLebensnummer(lebensnummer)
|
||||
if (existing != null) {
|
||||
throw IllegalArgumentException("A horse with life number '$lebensnummer' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return domPferdRepository.create(domPferd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing horse
|
||||
*/
|
||||
suspend fun updatePferd(id: Uuid, domPferd: DomPferd): DomPferd? {
|
||||
validatePferd(domPferd)
|
||||
|
||||
// Check if OEPS Satz number conflicts with another horse
|
||||
domPferd.oepsSatzNrPferd?.let { oepsNr ->
|
||||
val existing = domPferdRepository.findByOepsSatzNr(oepsNr)
|
||||
if (existing != null && existing.pferdId != id) {
|
||||
throw IllegalArgumentException("A horse with OEPS Satz number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if life number conflicts with another horse
|
||||
domPferd.lebensnummer?.let { lebensnummer ->
|
||||
val existing = domPferdRepository.findByLebensnummer(lebensnummer)
|
||||
if (existing != null && existing.pferdId != id) {
|
||||
throw IllegalArgumentException("A horse with life number '$lebensnummer' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return domPferdRepository.update(id, domPferd)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a horse by ID
|
||||
*/
|
||||
suspend fun deletePferd(id: Uuid): Boolean {
|
||||
return domPferdRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate horse data according to business rules
|
||||
*/
|
||||
private fun validatePferd(domPferd: DomPferd) {
|
||||
if (domPferd.name.isBlank()) {
|
||||
throw IllegalArgumentException("Horse name cannot be blank")
|
||||
}
|
||||
|
||||
if (domPferd.name.length > 100) {
|
||||
throw IllegalArgumentException("Horse name cannot exceed 100 characters")
|
||||
}
|
||||
|
||||
// Validate birth year if provided
|
||||
domPferd.geburtsjahr?.let { year ->
|
||||
if (year < 1900 || year > java.time.LocalDate.now().year) {
|
||||
throw IllegalArgumentException("Birth year must be between 1900 and current year")
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
domPferd.oepsSatzNrPferd?.let { oepsNr ->
|
||||
if (oepsNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Satz number cannot be blank if provided")
|
||||
}
|
||||
}
|
||||
|
||||
domPferd.lebensnummer?.let { lebensnummer ->
|
||||
if (lebensnummer.isBlank()) {
|
||||
throw IllegalArgumentException("Life number cannot be blank if provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.domaene.DomQualifikation
|
||||
import at.mocode.repositories.DomQualifikationRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Service layer for DomQualifikation (Domain Qualification) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class DomQualifikationService(private val domQualifikationRepository: DomQualifikationRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all qualifications
|
||||
*/
|
||||
suspend fun getAllQualifikationen(): List<DomQualifikation> {
|
||||
return domQualifikationRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a qualification by its unique identifier
|
||||
*/
|
||||
suspend fun getQualifikationById(id: Uuid): DomQualifikation? {
|
||||
return domQualifikationRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find qualifications by person ID
|
||||
*/
|
||||
suspend fun getQualifikationenByPersonId(personId: Uuid): List<DomQualifikation> {
|
||||
return domQualifikationRepository.findByPersonId(personId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find qualifications by qualification type ID
|
||||
*/
|
||||
suspend fun getQualifikationenByQualTypId(qualTypId: Uuid): List<DomQualifikation> {
|
||||
return domQualifikationRepository.findByQualTypId(qualTypId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find active qualifications by person ID
|
||||
*/
|
||||
suspend fun getActiveQualifikationenByPersonId(personId: Uuid): List<DomQualifikation> {
|
||||
return domQualifikationRepository.findActiveByPersonId(personId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find qualifications by validity period
|
||||
*/
|
||||
suspend fun getQualifikationenByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List<DomQualifikation> {
|
||||
// Validate date range if both dates are provided
|
||||
if (fromDate != null && toDate != null && fromDate > toDate) {
|
||||
throw IllegalArgumentException("From date must be before or equal to to date")
|
||||
}
|
||||
return domQualifikationRepository.findByValidityPeriod(fromDate, toDate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for qualifications by query string
|
||||
*/
|
||||
suspend fun searchQualifikationen(query: String): List<DomQualifikation> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return domQualifikationRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new qualification with business validation
|
||||
*/
|
||||
suspend fun createQualifikation(domQualifikation: DomQualifikation): DomQualifikation {
|
||||
validateQualifikation(domQualifikation)
|
||||
return domQualifikationRepository.create(domQualifikation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing qualification
|
||||
*/
|
||||
suspend fun updateQualifikation(id: Uuid, domQualifikation: DomQualifikation): DomQualifikation? {
|
||||
validateQualifikation(domQualifikation)
|
||||
return domQualifikationRepository.update(id, domQualifikation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a qualification by ID
|
||||
*/
|
||||
suspend fun deleteQualifikation(id: Uuid): Boolean {
|
||||
return domQualifikationRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a person has an active qualification of a specific type
|
||||
*/
|
||||
suspend fun hasActiveQualification(personId: Uuid, qualTypId: Uuid): Boolean {
|
||||
val activeQualifications = getActiveQualifikationenByPersonId(personId)
|
||||
return activeQualifications.any { it.qualTypId == qualTypId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current valid qualifications for a person
|
||||
*/
|
||||
suspend fun getCurrentValidQualifikationenByPersonId(personId: Uuid): List<DomQualifikation> {
|
||||
val currentJavaDate = java.time.LocalDate.now()
|
||||
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
|
||||
|
||||
val allPersonQualifications = getQualifikationenByPersonId(personId)
|
||||
return allPersonQualifications.filter { qualification ->
|
||||
qualification.istAktiv &&
|
||||
(qualification.gueltigVon == null || qualification.gueltigVon!! <= currentLocalDate) &&
|
||||
(qualification.gueltigBis == null || qualification.gueltigBis!! >= currentLocalDate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a qualification (soft delete)
|
||||
*/
|
||||
suspend fun deactivateQualifikation(id: Uuid): DomQualifikation? {
|
||||
val qualification = getQualifikationById(id)
|
||||
return if (qualification != null) {
|
||||
val updatedQualification = qualification.copy(istAktiv = false)
|
||||
updateQualifikation(id, updatedQualification)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate qualification data according to business rules
|
||||
*/
|
||||
private fun validateQualifikation(domQualifikation: DomQualifikation) {
|
||||
// Validate validity date range if both dates are provided
|
||||
if (domQualifikation.gueltigVon != null && domQualifikation.gueltigBis != null) {
|
||||
if (domQualifikation.gueltigVon!! > domQualifikation.gueltigBis!!) {
|
||||
throw IllegalArgumentException("Qualification validity start date must be before or equal to end date")
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that gueltigBis is not in the past for new active qualifications
|
||||
if (domQualifikation.istAktiv && domQualifikation.gueltigBis != null) {
|
||||
val currentJavaDate = java.time.LocalDate.now()
|
||||
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
|
||||
|
||||
if (domQualifikation.gueltigBis!! < currentLocalDate) {
|
||||
throw IllegalArgumentException("Cannot create active qualification with end date in the past")
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
domQualifikation.bemerkung?.let { bemerkung ->
|
||||
if (bemerkung.length > 1000) {
|
||||
throw IllegalArgumentException("Qualification remark cannot exceed 1000 characters")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.stammdaten.Person
|
||||
import at.mocode.repositories.PersonRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Person business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class PersonService(private val personRepository: PersonRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all persons
|
||||
*/
|
||||
suspend fun getAllPersons(): List<Person> {
|
||||
return personRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a person by their unique identifier
|
||||
*/
|
||||
suspend fun getPersonById(id: Uuid): Person? {
|
||||
return personRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a person by their OEPS Satz number
|
||||
*/
|
||||
suspend fun getPersonByOepsSatzNr(oepsSatzNr: String): Person? {
|
||||
if (oepsSatzNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Satz number cannot be blank")
|
||||
}
|
||||
return personRepository.findByOepsSatzNr(oepsSatzNr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persons by Verein (club) ID
|
||||
*/
|
||||
suspend fun getPersonsByVereinId(vereinId: Uuid): List<Person> {
|
||||
return personRepository.findByVereinId(vereinId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for persons by query string
|
||||
*/
|
||||
suspend fun searchPersons(query: String): List<Person> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return personRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new person with business validation
|
||||
*/
|
||||
suspend fun createPerson(person: Person): Person {
|
||||
validatePerson(person)
|
||||
|
||||
// Check if OEPS Satz number already exists
|
||||
person.oepsSatzNr?.let { oepsNr ->
|
||||
val existing = personRepository.findByOepsSatzNr(oepsNr)
|
||||
if (existing != null) {
|
||||
throw IllegalArgumentException("A person with OEPS Satz number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return personRepository.create(person)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing person
|
||||
*/
|
||||
suspend fun updatePerson(id: Uuid, person: Person): Person? {
|
||||
validatePerson(person)
|
||||
|
||||
// Check if OEPS Satz number conflicts with another person
|
||||
person.oepsSatzNr?.let { oepsNr ->
|
||||
val existing = personRepository.findByOepsSatzNr(oepsNr)
|
||||
if (existing != null && existing.id != id) {
|
||||
throw IllegalArgumentException("A person with OEPS Satz number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return personRepository.update(id, person)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a person by ID
|
||||
*/
|
||||
suspend fun deletePerson(id: Uuid): Boolean {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package at.mocode.services
|
||||
import at.mocode.repositories.*
|
||||
|
||||
/**
|
||||
* Service locator pattern for managing repository instances.
|
||||
* This provides a centralized way to access repository implementations
|
||||
* Service locator pattern for managing repository and service instances.
|
||||
* This provides a centralized way to access repository and service implementations
|
||||
* and makes it easier to switch implementations or add caching/decorators.
|
||||
*/
|
||||
object ServiceLocator {
|
||||
@@ -21,10 +21,23 @@ object ServiceLocator {
|
||||
val turnierRepository: TurnierRepository by lazy { PostgresTurnierRepository() }
|
||||
val veranstaltungRepository: VeranstaltungRepository by lazy { PostgresVeranstaltungRepository() }
|
||||
|
||||
// Service instances - lazy initialization with dependency injection
|
||||
val artikelService: ArtikelService by lazy { ArtikelService(artikelRepository) }
|
||||
val vereinService: VereinService by lazy { VereinService(vereinRepository) }
|
||||
val personService: PersonService by lazy { PersonService(personRepository) }
|
||||
val domLizenzService: DomLizenzService by lazy { DomLizenzService(domLizenzRepository) }
|
||||
val domPferdService: DomPferdService by lazy { DomPferdService(domPferdRepository) }
|
||||
val domQualifikationService: DomQualifikationService by lazy { DomQualifikationService(domQualifikationRepository) }
|
||||
val abteilungService: AbteilungService by lazy { AbteilungService(abteilungRepository) }
|
||||
val bewerbService: BewerbService by lazy { BewerbService(bewerbRepository) }
|
||||
val turnierService: TurnierService by lazy { TurnierService(turnierRepository) }
|
||||
val veranstaltungService: VeranstaltungService by lazy { VeranstaltungService(veranstaltungRepository) }
|
||||
|
||||
/**
|
||||
* Initialize all repositories - useful for eager loading or validation
|
||||
* Initialize all repositories and services - useful for eager loading or validation
|
||||
*/
|
||||
fun initializeAll() {
|
||||
// Initialize repositories
|
||||
artikelRepository
|
||||
vereinRepository
|
||||
personRepository
|
||||
@@ -35,5 +48,17 @@ object ServiceLocator {
|
||||
bewerbRepository
|
||||
turnierRepository
|
||||
veranstaltungRepository
|
||||
|
||||
// Initialize services
|
||||
artikelService
|
||||
vereinService
|
||||
personService
|
||||
domLizenzService
|
||||
domPferdService
|
||||
domQualifikationService
|
||||
abteilungService
|
||||
bewerbService
|
||||
turnierService
|
||||
veranstaltungService
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.Turnier
|
||||
import at.mocode.repositories.TurnierRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Turnier (Tournament) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class TurnierService(private val turnierRepository: TurnierRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all tournaments
|
||||
*/
|
||||
suspend fun getAllTurniere(): List<Turnier> {
|
||||
return turnierRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a tournament by its unique identifier
|
||||
*/
|
||||
suspend fun getTurnierById(id: Uuid): Turnier? {
|
||||
return turnierRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find tournaments by event (Veranstaltung) ID
|
||||
*/
|
||||
suspend fun getTurniereByVeranstaltungId(veranstaltungId: Uuid): List<Turnier> {
|
||||
return turnierRepository.findByVeranstaltungId(veranstaltungId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a tournament by its OEPS tournament number
|
||||
*/
|
||||
suspend fun getTurnierByOepsTurnierNr(oepsTurnierNr: String): Turnier? {
|
||||
if (oepsTurnierNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS tournament number cannot be blank")
|
||||
}
|
||||
return turnierRepository.findByOepsTurnierNr(oepsTurnierNr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for tournaments by query string
|
||||
*/
|
||||
suspend fun searchTurniere(query: String): List<Turnier> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return turnierRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tournament with business validation
|
||||
*/
|
||||
suspend fun createTurnier(turnier: Turnier): Turnier {
|
||||
validateTurnier(turnier)
|
||||
|
||||
// Check if OEPS tournament number already exists
|
||||
turnier.oepsTurnierNr?.let { oepsNr ->
|
||||
val existing = turnierRepository.findByOepsTurnierNr(oepsNr)
|
||||
if (existing != null) {
|
||||
throw IllegalArgumentException("A tournament with OEPS number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return turnierRepository.create(turnier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing tournament
|
||||
*/
|
||||
suspend fun updateTurnier(id: Uuid, turnier: Turnier): Turnier? {
|
||||
validateTurnier(turnier)
|
||||
|
||||
// Check if OEPS tournament number conflicts with another tournament
|
||||
turnier.oepsTurnierNr?.let { oepsNr ->
|
||||
val existing = turnierRepository.findByOepsTurnierNr(oepsNr)
|
||||
if (existing != null && existing.id != id) {
|
||||
throw IllegalArgumentException("A tournament with OEPS number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return turnierRepository.update(id, turnier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tournament by ID
|
||||
*/
|
||||
suspend fun deleteTurnier(id: Uuid): Boolean {
|
||||
return turnierRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tournaments for a specific event
|
||||
*/
|
||||
suspend fun getTurniereForEvent(veranstaltungId: Uuid): List<Turnier> {
|
||||
return getTurniereByVeranstaltungId(veranstaltungId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate tournament data according to business rules
|
||||
*/
|
||||
private fun validateTurnier(turnier: Turnier) {
|
||||
if (turnier.titel.isBlank()) {
|
||||
throw IllegalArgumentException("Tournament title cannot be blank")
|
||||
}
|
||||
|
||||
if (turnier.titel.length > 255) {
|
||||
throw IllegalArgumentException("Tournament title cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
if (turnier.datumVon > turnier.datumBis) {
|
||||
throw IllegalArgumentException("Tournament start date must be before or equal to end date")
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
if (turnier.oepsTurnierNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS tournament number cannot be blank")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.model.Veranstaltung
|
||||
import at.mocode.repositories.VeranstaltungRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Veranstaltung (Event) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class VeranstaltungService(private val veranstaltungRepository: VeranstaltungRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all events
|
||||
*/
|
||||
suspend fun getAllVeranstaltungen(): List<Veranstaltung> {
|
||||
return veranstaltungRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an event by its unique identifier
|
||||
*/
|
||||
suspend fun getVeranstaltungById(id: Uuid): Veranstaltung? {
|
||||
return veranstaltungRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find events by name
|
||||
*/
|
||||
suspend fun getVeranstaltungenByName(name: String): List<Veranstaltung> {
|
||||
if (name.isBlank()) {
|
||||
throw IllegalArgumentException("Event name cannot be blank")
|
||||
}
|
||||
return veranstaltungRepository.findByName(name.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Find events by organizer OEPS number
|
||||
*/
|
||||
suspend fun getVeranstaltungenByVeranstalterOepsNummer(oepsNummer: String): List<Veranstaltung> {
|
||||
if (oepsNummer.isBlank()) {
|
||||
throw IllegalArgumentException("Organizer OEPS number cannot be blank")
|
||||
}
|
||||
return veranstaltungRepository.findByVeranstalterOepsNummer(oepsNummer.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for events by query string
|
||||
*/
|
||||
suspend fun searchVeranstaltungen(query: String): List<Veranstaltung> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return veranstaltungRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new event with business validation
|
||||
*/
|
||||
suspend fun createVeranstaltung(veranstaltung: Veranstaltung): Veranstaltung {
|
||||
validateVeranstaltung(veranstaltung)
|
||||
return veranstaltungRepository.create(veranstaltung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing event
|
||||
*/
|
||||
suspend fun updateVeranstaltung(id: Uuid, veranstaltung: Veranstaltung): Veranstaltung? {
|
||||
validateVeranstaltung(veranstaltung)
|
||||
return veranstaltungRepository.update(id, veranstaltung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an event by ID
|
||||
*/
|
||||
suspend fun deleteVeranstaltung(id: Uuid): Boolean {
|
||||
return veranstaltungRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events happening in a specific year
|
||||
*/
|
||||
suspend fun getVeranstaltungenByYear(year: Int): List<Veranstaltung> {
|
||||
if (year < 1900 || year > 2100) {
|
||||
throw IllegalArgumentException("Year must be between 1900 and 2100")
|
||||
}
|
||||
|
||||
val allEvents = getAllVeranstaltungen()
|
||||
return allEvents.filter { event ->
|
||||
event.datumVon.year == year || event.datumBis.year == year ||
|
||||
(event.datumVon.year < year && event.datumBis.year > year)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current events (happening now or in the future)
|
||||
*/
|
||||
suspend fun getCurrentAndFutureVeranstaltungen(): List<Veranstaltung> {
|
||||
val currentJavaDate = java.time.LocalDate.now()
|
||||
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
|
||||
|
||||
val allEvents = getAllVeranstaltungen()
|
||||
return allEvents.filter { event ->
|
||||
event.datumBis >= currentLocalDate
|
||||
}.sortedBy { it.datumVon }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get past events
|
||||
*/
|
||||
suspend fun getPastVeranstaltungen(): List<Veranstaltung> {
|
||||
val currentJavaDate = java.time.LocalDate.now()
|
||||
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
|
||||
|
||||
val allEvents = getAllVeranstaltungen()
|
||||
return allEvents.filter { event ->
|
||||
event.datumBis < currentLocalDate
|
||||
}.sortedByDescending { it.datumVon }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by organizer name
|
||||
*/
|
||||
suspend fun getVeranstaltungenByVeranstalterName(veranstalterName: String): List<Veranstaltung> {
|
||||
if (veranstalterName.isBlank()) {
|
||||
throw IllegalArgumentException("Organizer name cannot be blank")
|
||||
}
|
||||
|
||||
val allEvents = getAllVeranstaltungen()
|
||||
return allEvents.filter { event ->
|
||||
event.veranstalterName.contains(veranstalterName.trim(), ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events by venue name
|
||||
*/
|
||||
suspend fun getVeranstaltungenByVenanstaltungsort(veranstaltungsortName: String): List<Veranstaltung> {
|
||||
if (veranstaltungsortName.isBlank()) {
|
||||
throw IllegalArgumentException("Venue name cannot be blank")
|
||||
}
|
||||
|
||||
val allEvents = getAllVeranstaltungen()
|
||||
return allEvents.filter { event ->
|
||||
event.veranstaltungsortName.contains(veranstaltungsortName.trim(), ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate event data according to business rules
|
||||
*/
|
||||
private fun validateVeranstaltung(veranstaltung: Veranstaltung) {
|
||||
if (veranstaltung.name.isBlank()) {
|
||||
throw IllegalArgumentException("Event name cannot be blank")
|
||||
}
|
||||
|
||||
if (veranstaltung.name.length > 255) {
|
||||
throw IllegalArgumentException("Event name cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
if (veranstaltung.veranstalterName.isBlank()) {
|
||||
throw IllegalArgumentException("Organizer name cannot be blank")
|
||||
}
|
||||
|
||||
if (veranstaltung.veranstalterName.length > 255) {
|
||||
throw IllegalArgumentException("Organizer name cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
if (veranstaltung.veranstaltungsortName.isBlank()) {
|
||||
throw IllegalArgumentException("Venue name cannot be blank")
|
||||
}
|
||||
|
||||
if (veranstaltung.veranstaltungsortName.length > 255) {
|
||||
throw IllegalArgumentException("Venue name cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
if (veranstaltung.veranstaltungsortAdresse.isBlank()) {
|
||||
throw IllegalArgumentException("Venue address cannot be blank")
|
||||
}
|
||||
|
||||
if (veranstaltung.veranstaltungsortAdresse.length > 500) {
|
||||
throw IllegalArgumentException("Venue address cannot exceed 500 characters")
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
if (veranstaltung.datumVon > veranstaltung.datumBis) {
|
||||
throw IllegalArgumentException("Event start date must be before or equal to end date")
|
||||
}
|
||||
|
||||
// Validate optional fields
|
||||
veranstaltung.veranstalterOepsNummer?.let { oepsNr ->
|
||||
if (oepsNr.isBlank()) {
|
||||
throw IllegalArgumentException("Organizer OEPS number cannot be blank if provided")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.kontaktpersonName?.let { name ->
|
||||
if (name.length > 255) {
|
||||
throw IllegalArgumentException("Contact person name cannot exceed 255 characters")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.kontaktTelefon?.let { telefon ->
|
||||
if (telefon.length > 50) {
|
||||
throw IllegalArgumentException("Contact phone cannot exceed 50 characters")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.kontaktEmail?.let { email ->
|
||||
if (email.length > 255) {
|
||||
throw IllegalArgumentException("Contact email cannot exceed 255 characters")
|
||||
}
|
||||
// Basic email validation
|
||||
if (!email.contains("@") || !email.contains(".")) {
|
||||
throw IllegalArgumentException("Contact email must be a valid email address")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.webseite?.let { webseite ->
|
||||
if (webseite.length > 500) {
|
||||
throw IllegalArgumentException("Website URL cannot exceed 500 characters")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.dsgvoText?.let { text ->
|
||||
if (text.length > 2000) {
|
||||
throw IllegalArgumentException("DSGVO text cannot exceed 2000 characters")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.haftungsText?.let { text ->
|
||||
if (text.length > 2000) {
|
||||
throw IllegalArgumentException("Liability text cannot exceed 2000 characters")
|
||||
}
|
||||
}
|
||||
|
||||
veranstaltung.sonstigeBesondereBestimmungen?.let { text ->
|
||||
if (text.length > 2000) {
|
||||
throw IllegalArgumentException("Special provisions text cannot exceed 2000 characters")
|
||||
}
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package at.mocode.services
|
||||
|
||||
import at.mocode.stammdaten.Verein
|
||||
import at.mocode.repositories.VereinRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Service layer for Verein (Club) business logic.
|
||||
* Handles business rules, validation, and coordinates with the repository layer.
|
||||
*/
|
||||
class VereinService(private val vereinRepository: VereinRepository) {
|
||||
|
||||
/**
|
||||
* Retrieve all clubs
|
||||
*/
|
||||
suspend fun getAllVereine(): List<Verein> {
|
||||
return vereinRepository.findAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a club by its unique identifier
|
||||
*/
|
||||
suspend fun getVereinById(id: Uuid): Verein? {
|
||||
return vereinRepository.findById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a club by its OEPS (Austrian Equestrian Federation) number
|
||||
*/
|
||||
suspend fun getVereinByOepsNr(oepsVereinsNr: String): Verein? {
|
||||
if (oepsVereinsNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Vereins number cannot be blank")
|
||||
}
|
||||
return vereinRepository.findByOepsVereinsNr(oepsVereinsNr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for clubs by query string
|
||||
*/
|
||||
suspend fun searchVereine(query: String): List<Verein> {
|
||||
if (query.isBlank()) {
|
||||
throw IllegalArgumentException("Search query cannot be blank")
|
||||
}
|
||||
return vereinRepository.search(query.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Find clubs by federal state (Bundesland)
|
||||
*/
|
||||
suspend fun getVereineByBundesland(bundesland: String): List<Verein> {
|
||||
if (bundesland.isBlank()) {
|
||||
throw IllegalArgumentException("Bundesland cannot be blank")
|
||||
}
|
||||
return vereinRepository.findByBundesland(bundesland)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new club with business validation
|
||||
*/
|
||||
suspend fun createVerein(verein: Verein): Verein {
|
||||
validateVerein(verein)
|
||||
|
||||
// Check if OEPS number already exists
|
||||
verein.oepsVereinsNr?.let { oepsNr ->
|
||||
val existing = vereinRepository.findByOepsVereinsNr(oepsNr)
|
||||
if (existing != null) {
|
||||
throw IllegalArgumentException("A club with OEPS number '$oepsNr' already exists")
|
||||
}
|
||||
}
|
||||
|
||||
return vereinRepository.create(verein)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing club
|
||||
*/
|
||||
suspend fun updateVerein(id: Uuid, verein: Verein): Verein? {
|
||||
validateVerein(verein)
|
||||
|
||||
// Check if 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")
|
||||
}
|
||||
}
|
||||
|
||||
return vereinRepository.update(id, verein)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a club by ID
|
||||
*/
|
||||
suspend fun deleteVerein(id: Uuid): Boolean {
|
||||
return vereinRepository.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate club data according to business rules
|
||||
*/
|
||||
private fun validateVerein(verein: Verein) {
|
||||
if (verein.name.isBlank()) {
|
||||
throw IllegalArgumentException("Club name cannot be blank")
|
||||
}
|
||||
|
||||
if (verein.name.length > 255) {
|
||||
throw IllegalArgumentException("Club name cannot exceed 255 characters")
|
||||
}
|
||||
|
||||
// Additional validation rules can be added here
|
||||
verein.oepsVereinsNr?.let { oepsNr ->
|
||||
if (oepsNr.isBlank()) {
|
||||
throw IllegalArgumentException("OEPS Vereins number cannot be blank if provided")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package at.mocode
|
||||
|
||||
import at.mocode.dto.ArtikelDto
|
||||
import at.mocode.dto.VereinDto
|
||||
import at.mocode.dto.base.VersionManager
|
||||
import at.mocode.dto.base.VersionValidationResult
|
||||
import at.mocode.dto.migrations.ArtikelDtoMigrator
|
||||
import com.benasher44.uuid.uuid4
|
||||
import com.ionspin.kotlin.bignum.decimal.BigDecimal
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class VersioningTest {
|
||||
|
||||
@Test
|
||||
fun testVersionManagerValidation() {
|
||||
// Test valid version
|
||||
val validResult = VersionManager.validateClientVersion("1.0")
|
||||
assertIs<VersionValidationResult.Valid>(validResult)
|
||||
assertEquals("1.0", validResult.version)
|
||||
|
||||
// Test unsupported version
|
||||
val unsupportedResult = VersionManager.validateClientVersion("2.0")
|
||||
assertIs<VersionValidationResult.UnsupportedVersion>(unsupportedResult)
|
||||
assertEquals("2.0", unsupportedResult.version)
|
||||
|
||||
// Test missing version
|
||||
val missingResult = VersionManager.validateClientVersion(null)
|
||||
assertIs<VersionValidationResult.MissingVersion>(missingResult)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVersionManagerInfo() {
|
||||
val versionInfo = VersionManager.getVersionInfo()
|
||||
assertEquals("1.0", versionInfo.apiVersion)
|
||||
assertTrue(versionInfo.supportedVersions.contains("1.0"))
|
||||
assertEquals("1.0", versionInfo.minimumClientVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testArtikelDtoVersioning() {
|
||||
val artikel = ArtikelDto(
|
||||
id = uuid4(),
|
||||
bezeichnung = "Test Artikel",
|
||||
preis = BigDecimal.fromInt(100),
|
||||
einheit = "Stück",
|
||||
istVerbandsabgabe = false,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
schemaVersion = "1.0",
|
||||
dataVersion = 1L
|
||||
)
|
||||
|
||||
assertEquals("1.0", artikel.schemaVersion)
|
||||
assertEquals(1L, artikel.dataVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVereinDtoVersioning() {
|
||||
val verein = VereinDto(
|
||||
id = uuid4(),
|
||||
oepsVereinsNr = "12345",
|
||||
name = "Test Verein",
|
||||
kuerzel = "TV",
|
||||
bundesland = "Wien",
|
||||
adresse = "Teststraße 1",
|
||||
plz = "1010",
|
||||
ort = "Wien",
|
||||
email = "test@verein.at",
|
||||
telefon = "+43123456789",
|
||||
webseite = "www.testverein.at",
|
||||
istAktiv = true,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
schemaVersion = "1.0",
|
||||
dataVersion = 1L
|
||||
)
|
||||
|
||||
assertEquals("1.0", verein.schemaVersion)
|
||||
assertEquals(1L, verein.dataVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testArtikelDtoMigrator() {
|
||||
val migrator = ArtikelDtoMigrator()
|
||||
|
||||
// Test migration capability
|
||||
assertTrue(migrator.canMigrate("1.0", "1.0"))
|
||||
|
||||
val artikel = ArtikelDto(
|
||||
id = uuid4(),
|
||||
bezeichnung = "Test Artikel",
|
||||
preis = BigDecimal.fromInt(100),
|
||||
einheit = "Stück",
|
||||
istVerbandsabgabe = false,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
schemaVersion = "1.0",
|
||||
dataVersion = 1L
|
||||
)
|
||||
|
||||
// Test migration (same version should return same object)
|
||||
val migratedArtikel = migrator.migrate(artikel, "1.0", "1.0")
|
||||
assertEquals(artikel, migratedArtikel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testVersionSupport() {
|
||||
assertTrue(VersionManager.isVersionSupported("1.0"))
|
||||
assertTrue(!VersionManager.isVersionSupported("2.0"))
|
||||
assertTrue(!VersionManager.isVersionDeprecated("1.0"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user