(fix) Implementiere einen Service-Layer

Erstellung von DTOs für alle Ressourcen
Implement a versioning system
This commit is contained in:
2025-06-30 22:25:35 +02:00
parent 418e692092
commit e2432510af
28 changed files with 4102 additions and 14 deletions
@@ -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"))
}
}