feat: integrate new desktop shell and extend backend & ADRs
- Added `meldestelle-desktop` module using JVM/Compose Desktop, registered in `settings.gradle.kts`. - Integrated new screens and desktop navigation into core: `Veranstaltungen`, `TurnierDetail`, etc. - Expanded backend with `ExposedFunktionaerRepository` in `officials-infrastructure`. - Completed ADRs for bounded context mapping (`ADR-0014`) and context map (`ADR-0015`). - Updated and extended project documentation with session logs and architecture decisions. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -14,6 +14,9 @@ dependencies {
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.entries.entriesApi)
|
||||
implementation(projects.backend.services.entries.entriesDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
||||
implementation(projects.backend.infrastructure.security)
|
||||
|
||||
@@ -38,6 +41,10 @@ dependencies {
|
||||
implementation(libs.spring.boot.starter.aop)
|
||||
|
||||
implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
// Exposed ORM für Datenbankzugriff
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.service
|
||||
|
||||
import at.mocode.entries.api.*
|
||||
import at.mocode.entries.service.usecase.NennungUseCases
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* REST-Controller für den Nennungs-Workflow (registration-context).
|
||||
*
|
||||
* Basis-URL: /api/v1/registrations/nennungen
|
||||
*
|
||||
* Endpunkte:
|
||||
* GET / → Liste aller Nennungen (gefiltert)
|
||||
* GET /{nennungsId} → Nennung-Detail
|
||||
* POST / → Neue Nennung einreichen
|
||||
* PUT /{nennungsId}/status → Status ändern
|
||||
* DELETE /{nennungsId} → Nennung zurückziehen
|
||||
* POST /{nennungsId}/transfer → Nennungs-Transfer
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/registrations/nennungen")
|
||||
@CrossOrigin(
|
||||
origins = ["http://localhost:8080", "http://localhost:8083", "http://localhost:4000"],
|
||||
methods = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS],
|
||||
allowedHeaders = ["*"],
|
||||
allowCredentials = "true"
|
||||
)
|
||||
@Tag(name = "Nennungen", description = "Nennungs-Workflow (registration-context)")
|
||||
class NennungController(
|
||||
private val useCases: NennungUseCases
|
||||
) {
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "Nennungen abrufen (gefiltert nach turnierId, bewerbId, abteilungId oder reiterId)")
|
||||
suspend fun getNennungen(
|
||||
@RequestParam(required = false) turnierId: String?,
|
||||
@RequestParam(required = false) bewerbId: String?,
|
||||
@RequestParam(required = false) abteilungId: String?,
|
||||
@RequestParam(required = false) reiterId: String?
|
||||
): List<NennungSummaryDto> = when {
|
||||
turnierId != null -> useCases.getNennungenByTurnier(Uuid.parse(turnierId))
|
||||
bewerbId != null -> useCases.getNennungenByBewerb(Uuid.parse(bewerbId))
|
||||
abteilungId != null -> useCases.getNennungenByAbteilung(Uuid.parse(abteilungId))
|
||||
reiterId != null -> useCases.getNennungenByReiter(Uuid.parse(reiterId))
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
@GetMapping("/{nennungsId}")
|
||||
@Operation(summary = "Nennung-Detail abrufen")
|
||||
suspend fun getNennung(@PathVariable nennungsId: String): NennungDetailDto =
|
||||
useCases.getNennungById(Uuid.parse(nennungsId))
|
||||
?: throw NoSuchElementException("Nennung nicht gefunden: $nennungsId")
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "Neue Nennung einreichen")
|
||||
suspend fun nennungEinreichen(
|
||||
@RequestBody request: NennungEinreichenRequest
|
||||
): NennungDetailDto = useCases.nennungEinreichen(request)
|
||||
|
||||
@PutMapping("/{nennungsId}/status")
|
||||
@Operation(summary = "Nennungs-Status ändern (z.B. BESTAETIGT, ABGELEHNT)")
|
||||
suspend fun statusAendern(
|
||||
@PathVariable nennungsId: String,
|
||||
@RequestBody request: NennungStatusAendernRequest
|
||||
): NennungDetailDto = useCases.statusAendern(Uuid.parse(nennungsId), request)
|
||||
|
||||
@DeleteMapping("/{nennungsId}")
|
||||
@Operation(summary = "Nennung zurückziehen (Status → ZURUECKGEZOGEN)")
|
||||
suspend fun nennungZurueckziehen(
|
||||
@PathVariable nennungsId: String
|
||||
): NennungDetailDto = useCases.nennungZurueckziehen(Uuid.parse(nennungsId))
|
||||
|
||||
@PostMapping("/{nennungsId}/transfer")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "Nennungs-Transfer durchführen (atomare Operation gemäß ÖTO)")
|
||||
suspend fun nennungTransferieren(
|
||||
@PathVariable nennungsId: String,
|
||||
@RequestBody request: NennungTransferRequest
|
||||
): NennungsTransferDto = useCases.nennungTransferieren(Uuid.parse(nennungsId), request)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package at.mocode.entries.service.config
|
||||
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
||||
import at.mocode.entries.service.persistence.NennungRepositoryImpl
|
||||
import at.mocode.entries.service.persistence.NennungsTransferRepositoryImpl
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
* Spring-Bean-Konfiguration für den Entries Service.
|
||||
*
|
||||
* Registriert die Repository-Implementierungen als Spring-Beans,
|
||||
* damit sie in NennungUseCases per Constructor-Injection verfügbar sind.
|
||||
*/
|
||||
@Configuration
|
||||
class EntriesBeansConfiguration {
|
||||
|
||||
@Bean
|
||||
fun nennungRepository(): NennungRepository = NennungRepositoryImpl()
|
||||
|
||||
@Bean
|
||||
fun nennungsTransferRepository(): NennungsTransferRepository = NennungsTransferRepositoryImpl()
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package at.mocode.entries.service.config
|
||||
|
||||
import at.mocode.entries.service.persistence.NennungTable
|
||||
import at.mocode.entries.service.persistence.NennungsTransferTable
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
||||
/**
|
||||
* Datenbank-Konfiguration für den Entries Service.
|
||||
*
|
||||
* Initialisiert das Exposed-Schema für Nennungen und Nennungs-Transfers.
|
||||
* Die DB-Verbindung selbst wird durch den zentralen DataSource-Bean initialisiert.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("!test")
|
||||
class EntriesDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EntriesDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initialisiere Datenbank-Schema für Entries Service...")
|
||||
try {
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
NennungTable,
|
||||
NennungsTransferTable
|
||||
)
|
||||
log.info("Entries Datenbank-Schema erfolgreich initialisiert")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Fehler beim Initialisieren des Datenbank-Schemas", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package at.mocode.entries.service.config
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ProblemDetail
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
|
||||
/**
|
||||
* Globaler Exception-Handler für den Entries Service.
|
||||
*
|
||||
* Mappt Domain-Exceptions auf HTTP-Statuscodes gemäß RFC 9457 (Problem Details).
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
class EntriesExceptionHandler {
|
||||
|
||||
private val log = LoggerFactory.getLogger(EntriesExceptionHandler::class.java)
|
||||
|
||||
@ExceptionHandler(NoSuchElementException::class)
|
||||
fun handleNotFound(ex: NoSuchElementException): ProblemDetail {
|
||||
log.warn("Ressource nicht gefunden: {}", ex.message)
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.message ?: "Nicht gefunden")
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException::class)
|
||||
fun handleBadRequest(ex: IllegalArgumentException): ProblemDetail {
|
||||
log.warn("Ungültige Anfrage: {}", ex.message)
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.message ?: "Ungültige Anfrage")
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException::class)
|
||||
fun handleConflict(ex: IllegalStateException): ProblemDetail {
|
||||
log.warn("Konflikt: {}", ex.message)
|
||||
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.message ?: "Konflikt")
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.service.persistence
|
||||
|
||||
import at.mocode.core.domain.model.NennungsStatusE
|
||||
import at.mocode.core.domain.model.StartwunschE
|
||||
import at.mocode.entries.domain.model.DomNennung
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import kotlin.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
/**
|
||||
* Exposed-basierte Implementierung des NennungRepository.
|
||||
*/
|
||||
class NennungRepositoryImpl : NennungRepository {
|
||||
|
||||
private fun rowToNennung(row: ResultRow): DomNennung = DomNennung(
|
||||
nennungId = row[NennungTable.id].toKotlinUuid(),
|
||||
abteilungId = row[NennungTable.abteilungId].toKotlinUuid(),
|
||||
bewerbId = row[NennungTable.bewerbId].toKotlinUuid(),
|
||||
turnierId = row[NennungTable.turnierId].toKotlinUuid(),
|
||||
reiterId = row[NennungTable.reiterId].toKotlinUuid(),
|
||||
pferdId = row[NennungTable.pferdId].toKotlinUuid(),
|
||||
zahlerId = row[NennungTable.zahlerId]?.toKotlinUuid(),
|
||||
status = NennungsStatusE.valueOf(row[NennungTable.status]),
|
||||
startwunsch = StartwunschE.valueOf(row[NennungTable.startwunsch]),
|
||||
istNachnennung = row[NennungTable.istNachnennung],
|
||||
nachnenngebuehrErlassen = row[NennungTable.nachnenngebuehrErlassen],
|
||||
bemerkungen = row[NennungTable.bemerkungen],
|
||||
createdAt = row[NennungTable.createdAt],
|
||||
updatedAt = row[NennungTable.updatedAt]
|
||||
)
|
||||
|
||||
override suspend fun findById(id: Uuid): DomNennung? = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.id eq id.toJavaUuid() }
|
||||
.map(::rowToNennung)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByBewerbId(bewerbId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.bewerbId eq bewerbId.toJavaUuid() }
|
||||
.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findByAbteilungId(abteilungId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.abteilungId eq abteilungId.toJavaUuid() }
|
||||
.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findByTurnierId(turnierId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.turnierId eq turnierId.toJavaUuid() }
|
||||
.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findByReiterId(reiterId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.reiterId eq reiterId.toJavaUuid() }
|
||||
.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findByPferdId(pferdId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.pferdId eq pferdId.toJavaUuid() }
|
||||
.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findByReiterIdAndTurnierId(reiterId: Uuid, turnierId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where {
|
||||
(NennungTable.reiterId eq reiterId.toJavaUuid()) and
|
||||
(NennungTable.turnierId eq turnierId.toJavaUuid())
|
||||
}.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findByStatus(status: NennungsStatusE): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.status eq status.name }
|
||||
.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun findNachnennungenByBewerbId(bewerbId: Uuid): List<DomNennung> = transaction {
|
||||
NennungTable.selectAll().where {
|
||||
(NennungTable.bewerbId eq bewerbId.toJavaUuid()) and
|
||||
(NennungTable.istNachnennung eq true)
|
||||
}.map(::rowToNennung)
|
||||
}
|
||||
|
||||
override suspend fun save(nennung: DomNennung): DomNennung = transaction {
|
||||
val now = Clock.System.now()
|
||||
val existing = NennungTable.selectAll()
|
||||
.where { NennungTable.id eq nennung.nennungId.toJavaUuid() }
|
||||
.singleOrNull()
|
||||
if (existing == null) {
|
||||
NennungTable.insert { stmt ->
|
||||
stmt[id] = nennung.nennungId.toJavaUuid()
|
||||
stmt[abteilungId] = nennung.abteilungId.toJavaUuid()
|
||||
stmt[bewerbId] = nennung.bewerbId.toJavaUuid()
|
||||
stmt[turnierId] = nennung.turnierId.toJavaUuid()
|
||||
stmt[reiterId] = nennung.reiterId.toJavaUuid()
|
||||
stmt[pferdId] = nennung.pferdId.toJavaUuid()
|
||||
stmt[zahlerId] = nennung.zahlerId?.toJavaUuid()
|
||||
stmt[status] = nennung.status.name
|
||||
stmt[startwunsch] = nennung.startwunsch.name
|
||||
stmt[istNachnennung] = nennung.istNachnennung
|
||||
stmt[nachnenngebuehrErlassen] = nennung.nachnenngebuehrErlassen
|
||||
stmt[bemerkungen] = nennung.bemerkungen
|
||||
stmt[createdAt] = nennung.createdAt
|
||||
stmt[updatedAt] = now
|
||||
}
|
||||
nennung.copy(updatedAt = now)
|
||||
} else {
|
||||
NennungTable.update({ NennungTable.id eq nennung.nennungId.toJavaUuid() }) { stmt ->
|
||||
stmt[abteilungId] = nennung.abteilungId.toJavaUuid()
|
||||
stmt[bewerbId] = nennung.bewerbId.toJavaUuid()
|
||||
stmt[turnierId] = nennung.turnierId.toJavaUuid()
|
||||
stmt[reiterId] = nennung.reiterId.toJavaUuid()
|
||||
stmt[pferdId] = nennung.pferdId.toJavaUuid()
|
||||
stmt[zahlerId] = nennung.zahlerId?.toJavaUuid()
|
||||
stmt[status] = nennung.status.name
|
||||
stmt[startwunsch] = nennung.startwunsch.name
|
||||
stmt[istNachnennung] = nennung.istNachnennung
|
||||
stmt[nachnenngebuehrErlassen] = nennung.nachnenngebuehrErlassen
|
||||
stmt[bemerkungen] = nennung.bemerkungen
|
||||
stmt[updatedAt] = now
|
||||
}
|
||||
nennung.copy(updatedAt = now)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
||||
NennungTable.deleteWhere { NennungTable.id eq id.toJavaUuid() } > 0
|
||||
}
|
||||
|
||||
override suspend fun countByBewerbId(bewerbId: Uuid): Long = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.bewerbId eq bewerbId.toJavaUuid() }.count()
|
||||
}
|
||||
|
||||
override suspend fun countByAbteilungId(abteilungId: Uuid): Long = transaction {
|
||||
NennungTable.selectAll().where { NennungTable.abteilungId eq abteilungId.toJavaUuid() }.count()
|
||||
}
|
||||
|
||||
override suspend fun countByTurnierIdAndStatus(turnierId: Uuid, status: NennungsStatusE): Long = transaction {
|
||||
NennungTable.selectAll().where {
|
||||
(NennungTable.turnierId eq turnierId.toJavaUuid()) and
|
||||
(NennungTable.status eq status.name)
|
||||
}.count()
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package at.mocode.entries.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für Nennungen (registration-context).
|
||||
*/
|
||||
object NennungTable : Table("nennungen") {
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
|
||||
// Competition References
|
||||
val abteilungId = javaUUID("abteilung_id")
|
||||
val bewerbId = javaUUID("bewerb_id")
|
||||
val turnierId = javaUUID("turnier_id")
|
||||
|
||||
// Actor References (actor-context)
|
||||
val reiterId = javaUUID("reiter_id")
|
||||
val pferdId = javaUUID("pferd_id")
|
||||
|
||||
// Billing Reference (billing-context)
|
||||
val zahlerId = javaUUID("zahler_id").nullable()
|
||||
|
||||
// Entry Details
|
||||
val status = varchar("status", 50)
|
||||
val startwunsch = varchar("startwunsch", 50)
|
||||
val istNachnennung = bool("ist_nachnennung").default(false)
|
||||
val nachnenngebuehrErlassen = bool("nachnenngebuehr_erlassen").default(false)
|
||||
val bemerkungen = text("bemerkungen").nullable()
|
||||
|
||||
// Audit Fields
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, turnierId)
|
||||
index(false, bewerbId)
|
||||
index(false, abteilungId)
|
||||
index(false, reiterId)
|
||||
index(false, pferdId)
|
||||
index(false, status)
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.service.persistence
|
||||
|
||||
import at.mocode.entries.domain.model.DomNennungsTransfer
|
||||
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import kotlin.time.Clock
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
/**
|
||||
* Exposed-basierte Implementierung des NennungsTransferRepository.
|
||||
*/
|
||||
class NennungsTransferRepositoryImpl : NennungsTransferRepository {
|
||||
|
||||
private fun rowToTransfer(row: ResultRow): DomNennungsTransfer = DomNennungsTransfer(
|
||||
transferId = row[NennungsTransferTable.id].toKotlinUuid(),
|
||||
ursprungsNennungId = row[NennungsTransferTable.ursprungsNennungId].toKotlinUuid(),
|
||||
neueNennungId = row[NennungsTransferTable.neueNennungId].toKotlinUuid(),
|
||||
alterReiterId = row[NennungsTransferTable.alterReiterId]?.toKotlinUuid(),
|
||||
neuerReiterId = row[NennungsTransferTable.neuerReiterId]?.toKotlinUuid(),
|
||||
altesPferdId = row[NennungsTransferTable.altesPferdId]?.toKotlinUuid(),
|
||||
neuesPferdId = row[NennungsTransferTable.neuesPferdId]?.toKotlinUuid(),
|
||||
istNachNennschluss = row[NennungsTransferTable.istNachNennschluss],
|
||||
nachnenngebuehrErlassen = row[NennungsTransferTable.nachnenngebuehrErlassen],
|
||||
autorisiertVon = row[NennungsTransferTable.autorisiertVon].toKotlinUuid(),
|
||||
grund = row[NennungsTransferTable.grund],
|
||||
createdAt = row[NennungsTransferTable.createdAt]
|
||||
)
|
||||
|
||||
override suspend fun findById(id: Uuid): DomNennungsTransfer? = transaction {
|
||||
NennungsTransferTable.selectAll().where { NennungsTransferTable.id eq id.toJavaUuid() }
|
||||
.map(::rowToTransfer)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByUrsprungsNennungId(nennungId: Uuid): List<DomNennungsTransfer> = transaction {
|
||||
NennungsTransferTable.selectAll()
|
||||
.where { NennungsTransferTable.ursprungsNennungId eq nennungId.toJavaUuid() }
|
||||
.map(::rowToTransfer)
|
||||
}
|
||||
|
||||
override suspend fun save(transfer: DomNennungsTransfer): DomNennungsTransfer = transaction {
|
||||
val now = Clock.System.now()
|
||||
NennungsTransferTable.insert { stmt ->
|
||||
stmt[id] = transfer.transferId.toJavaUuid()
|
||||
stmt[ursprungsNennungId] = transfer.ursprungsNennungId.toJavaUuid()
|
||||
stmt[neueNennungId] = transfer.neueNennungId.toJavaUuid()
|
||||
stmt[alterReiterId] = transfer.alterReiterId?.toJavaUuid()
|
||||
stmt[neuerReiterId] = transfer.neuerReiterId?.toJavaUuid()
|
||||
stmt[altesPferdId] = transfer.altesPferdId?.toJavaUuid()
|
||||
stmt[neuesPferdId] = transfer.neuesPferdId?.toJavaUuid()
|
||||
stmt[istNachNennschluss] = transfer.istNachNennschluss
|
||||
stmt[nachnenngebuehrErlassen] = transfer.nachnenngebuehrErlassen
|
||||
stmt[autorisiertVon] = transfer.autorisiertVon.toJavaUuid()
|
||||
stmt[grund] = transfer.grund
|
||||
stmt[createdAt] = now
|
||||
}
|
||||
transfer.copy(createdAt = now)
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package at.mocode.entries.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für Nennungs-Transfers (registration-context).
|
||||
*
|
||||
* Ein Transfer ist KEIN Storno + Neunennung, sondern eine atomare Operation
|
||||
* auf der bestehenden Nennung. Die ursprüngliche Nennung erhält Status TRANSFERIERT.
|
||||
*/
|
||||
object NennungsTransferTable : Table("nennungs_transfers") {
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
|
||||
// Linked Entries
|
||||
val ursprungsNennungId = javaUUID("ursprungs_nennung_id")
|
||||
val neueNennungId = javaUUID("neue_nennung_id")
|
||||
|
||||
// What changed – Rider
|
||||
val alterReiterId = javaUUID("alter_reiter_id").nullable()
|
||||
val neuerReiterId = javaUUID("neuer_reiter_id").nullable()
|
||||
|
||||
// What changed – Horse
|
||||
val altesPferdId = javaUUID("altes_pferd_id").nullable()
|
||||
val neuesPferdId = javaUUID("neues_pferd_id").nullable()
|
||||
|
||||
// Timing & Fees
|
||||
val istNachNennschluss = bool("ist_nach_nennschluss").default(false)
|
||||
val nachnenngebuehrErlassen = bool("nachnenngebuehr_erlassen").default(false)
|
||||
|
||||
// Authorization (Override-Event)
|
||||
val autorisiertVon = javaUUID("autorisiert_von")
|
||||
val grund = text("grund").nullable()
|
||||
|
||||
// Audit
|
||||
val createdAt = timestamp("created_at")
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index(false, ursprungsNennungId)
|
||||
index(false, neueNennungId)
|
||||
index(false, autorisiertVon)
|
||||
}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.service.usecase
|
||||
|
||||
import at.mocode.core.domain.model.NennungsStatusE
|
||||
import at.mocode.entries.api.*
|
||||
import at.mocode.entries.domain.model.DomNennung
|
||||
import at.mocode.entries.domain.model.DomNennungsTransfer
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use Cases für den Nennungs-Workflow (registration-context).
|
||||
*
|
||||
* Kapselt die fachliche Logik gemäß ÖTO-Regelwerk:
|
||||
* - Warn-Logik statt harter Fehler (TBA hat das letzte Wort)
|
||||
* - Transfer = atomare Operation, kein Storno + Neunennung
|
||||
* - Nachnenngebühr kann vom Veranstalter erlassen werden
|
||||
*/
|
||||
@Service
|
||||
class NennungUseCases(
|
||||
private val nennungRepository: NennungRepository,
|
||||
private val transferRepository: NennungsTransferRepository
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(NennungUseCases::class.java)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
suspend fun getNennungById(id: Uuid): NennungDetailDto? =
|
||||
nennungRepository.findById(id)?.toDetailDto()
|
||||
|
||||
suspend fun getNennungenByTurnier(turnierId: Uuid): List<NennungSummaryDto> =
|
||||
nennungRepository.findByTurnierId(turnierId).map { it.toSummaryDto() }
|
||||
|
||||
suspend fun getNennungenByBewerb(bewerbId: Uuid): List<NennungSummaryDto> =
|
||||
nennungRepository.findByBewerbId(bewerbId).map { it.toSummaryDto() }
|
||||
|
||||
suspend fun getNennungenByAbteilung(abteilungId: Uuid): List<NennungSummaryDto> =
|
||||
nennungRepository.findByAbteilungId(abteilungId).map { it.toSummaryDto() }
|
||||
|
||||
suspend fun getNennungenByReiter(reiterId: Uuid): List<NennungSummaryDto> =
|
||||
nennungRepository.findByReiterId(reiterId).map { it.toSummaryDto() }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reicht eine neue Nennung ein (POST).
|
||||
* Gibt eine Warnung aus wenn Nachnennung, aber blockiert nicht.
|
||||
*/
|
||||
suspend fun nennungEinreichen(request: NennungEinreichenRequest): NennungDetailDto {
|
||||
if (request.istNachnennung) {
|
||||
log.warn(
|
||||
"NACHNENNUNG eingereicht für Turnier={} Bewerb={} Reiter={}",
|
||||
request.turnierId, request.bewerbId, request.reiterId
|
||||
)
|
||||
}
|
||||
val nennung = DomNennung(
|
||||
abteilungId = request.abteilungId,
|
||||
bewerbId = request.bewerbId,
|
||||
turnierId = request.turnierId,
|
||||
reiterId = request.reiterId,
|
||||
pferdId = request.pferdId,
|
||||
zahlerId = request.zahlerId,
|
||||
startwunsch = request.startwunsch,
|
||||
istNachnennung = request.istNachnennung,
|
||||
bemerkungen = request.bemerkungen
|
||||
)
|
||||
val saved = nennungRepository.save(nennung)
|
||||
log.info("Nennung eingereicht: nennungId={} turnierId={}", saved.nennungId, saved.turnierId)
|
||||
return saved.toDetailDto()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ändert den Status einer Nennung (PUT).
|
||||
*/
|
||||
suspend fun statusAendern(nennungId: Uuid, request: NennungStatusAendernRequest): NennungDetailDto {
|
||||
val nennung = nennungRepository.findById(nennungId)
|
||||
?: throw NoSuchElementException("Nennung nicht gefunden: $nennungId")
|
||||
|
||||
val updated = nennung.copy(
|
||||
status = request.neuerStatus,
|
||||
bemerkungen = request.bemerkungen ?: nennung.bemerkungen
|
||||
).withUpdatedTimestamp()
|
||||
|
||||
val saved = nennungRepository.save(updated)
|
||||
log.info("Nennungs-Status geändert: nennungId={} status={}", nennungId, request.neuerStatus)
|
||||
return saved.toDetailDto()
|
||||
}
|
||||
|
||||
/**
|
||||
* Zieht eine Nennung zurück (DELETE → Status ZURUECKGEZOGEN).
|
||||
*/
|
||||
suspend fun nennungZurueckziehen(nennungId: Uuid): NennungDetailDto {
|
||||
val nennung = nennungRepository.findById(nennungId)
|
||||
?: throw NoSuchElementException("Nennung nicht gefunden: $nennungId")
|
||||
|
||||
val updated = nennung.copy(status = NennungsStatusE.ZURUECKGEZOGEN).withUpdatedTimestamp()
|
||||
val saved = nennungRepository.save(updated)
|
||||
log.info("Nennung zurückgezogen: nennungId={}", nennungId)
|
||||
return saved.toDetailDto()
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen Nennungs-Transfer durch (POST /transfer).
|
||||
*
|
||||
* Atomare Operation gemäß ÖTO:
|
||||
* 1. Ursprungs-Nennung → Status TRANSFERIERT
|
||||
* 2. Neue Nennung für neues Reiter-Pferd-Paar anlegen
|
||||
* 3. Transfer-Record als Audit-Trail speichern
|
||||
*/
|
||||
suspend fun nennungTransferieren(nennungId: Uuid, request: NennungTransferRequest): NennungsTransferDto {
|
||||
val ursprung = nennungRepository.findById(nennungId)
|
||||
?: throw NoSuchElementException("Nennung nicht gefunden: $nennungId")
|
||||
|
||||
if (request.neuerReiterId == null && request.neuesPferdId == null) {
|
||||
throw IllegalArgumentException("Transfer erfordert mindestens eine Änderung (Reiter oder Pferd)")
|
||||
}
|
||||
|
||||
if (request.istNachNennschluss) {
|
||||
log.warn(
|
||||
"Transfer NACH NENNSCHLUSS: nennungId={} autorisiertVon={}",
|
||||
nennungId, request.autorisiertVon
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Ursprungs-Nennung schließen
|
||||
val geschlosseneNennung = ursprung.copy(status = NennungsStatusE.TRANSFERIERT).withUpdatedTimestamp()
|
||||
nennungRepository.save(geschlosseneNennung)
|
||||
|
||||
// 2. Neue Nennung anlegen
|
||||
val neueNennung = DomNennung(
|
||||
abteilungId = ursprung.abteilungId,
|
||||
bewerbId = ursprung.bewerbId,
|
||||
turnierId = ursprung.turnierId,
|
||||
reiterId = request.neuerReiterId ?: ursprung.reiterId,
|
||||
pferdId = request.neuesPferdId ?: ursprung.pferdId,
|
||||
zahlerId = ursprung.zahlerId,
|
||||
startwunsch = ursprung.startwunsch,
|
||||
istNachnennung = request.istNachNennschluss,
|
||||
nachnenngebuehrErlassen = request.nachnenngebuehrErlassen,
|
||||
bemerkungen = ursprung.bemerkungen
|
||||
)
|
||||
val gespeicherteNeueNennung = nennungRepository.save(neueNennung)
|
||||
|
||||
// 3. Transfer-Record speichern
|
||||
val transfer = DomNennungsTransfer(
|
||||
ursprungsNennungId = ursprung.nennungId,
|
||||
neueNennungId = gespeicherteNeueNennung.nennungId,
|
||||
alterReiterId = if (request.neuerReiterId != null) ursprung.reiterId else null,
|
||||
neuerReiterId = request.neuerReiterId,
|
||||
altesPferdId = if (request.neuesPferdId != null) ursprung.pferdId else null,
|
||||
neuesPferdId = request.neuesPferdId,
|
||||
istNachNennschluss = request.istNachNennschluss,
|
||||
nachnenngebuehrErlassen = request.nachnenngebuehrErlassen,
|
||||
autorisiertVon = request.autorisiertVon,
|
||||
grund = request.grund
|
||||
)
|
||||
val gespeicherterTransfer = transferRepository.save(transfer)
|
||||
|
||||
log.info(
|
||||
"Nennungs-Transfer abgeschlossen: ursprung={} neu={} transferId={}",
|
||||
nennungId, gespeicherteNeueNennung.nennungId, gespeicherterTransfer.transferId
|
||||
)
|
||||
return gespeicherterTransfer.toDto()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mapping Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private fun DomNennung.toDetailDto() = NennungDetailDto(
|
||||
nennungId = nennungId,
|
||||
abteilungId = abteilungId,
|
||||
bewerbId = bewerbId,
|
||||
turnierId = turnierId,
|
||||
reiterId = reiterId,
|
||||
pferdId = pferdId,
|
||||
zahlerId = zahlerId,
|
||||
status = status,
|
||||
startwunsch = startwunsch,
|
||||
istNachnennung = istNachnennung,
|
||||
nachnenngebuehrErlassen = nachnenngebuehrErlassen,
|
||||
isNachnenngebuehrFaellig = isNachnenngebuehrFaellig(),
|
||||
bemerkungen = bemerkungen,
|
||||
createdAt = createdAt.toString(),
|
||||
updatedAt = updatedAt.toString()
|
||||
)
|
||||
|
||||
private fun DomNennung.toSummaryDto() = NennungSummaryDto(
|
||||
nennungId = nennungId,
|
||||
turnierId = turnierId,
|
||||
bewerbId = bewerbId,
|
||||
abteilungId = abteilungId,
|
||||
reiterId = reiterId,
|
||||
pferdId = pferdId,
|
||||
status = status,
|
||||
istNachnennung = istNachnennung,
|
||||
createdAt = createdAt.toString()
|
||||
)
|
||||
|
||||
private fun DomNennungsTransfer.toDto() = NennungsTransferDto(
|
||||
transferId = transferId,
|
||||
ursprungsNennungId = ursprungsNennungId,
|
||||
neueNennungId = neueNennungId,
|
||||
alterReiterId = alterReiterId,
|
||||
neuerReiterId = neuerReiterId,
|
||||
altesPferdId = altesPferdId,
|
||||
neuesPferdId = neuesPferdId,
|
||||
istNachNennschluss = istNachNennschluss,
|
||||
nachnenngebuehrErlassen = nachnenngebuehrErlassen,
|
||||
autorisiertVon = autorisiertVon,
|
||||
grund = grund,
|
||||
createdAt = createdAt.toString()
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user