docs: Migrationsplan für Projekt-Restrukturierung hinzugefügt

- Detaillierter Plan zur Migration von alter zu neuer Modulstruktur
- Umfasst Überführung von shared-kernel zu core-Modulen
- Definiert Migration von Fachdomänen zu bounded contexts:
  * master-data → masterdata-Module
  * member-management → members-Module
  * horse-registry → horses-Module
  * event-management → events-Module
- Beschreibt Verlagerung von api-gateway zu infrastructure/gateway
- Strukturiert nach Domain-driven Design Prinzipien
- Berücksichtigt Clean Architecture Layering (domain, application, infrastructure, api)
This commit is contained in:
stefan
2025-07-25 13:05:42 +02:00
parent a4c7d53aa3
commit 65a0084f91
68 changed files with 13107 additions and 101 deletions
@@ -0,0 +1,390 @@
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.masterdata.domain.repository.AltersklasseRepository
import at.mocode.core.utils.validation.ValidationResult
import at.mocode.core.utils.validation.ValidationError
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
/**
* Use case for creating and updating age class information.
*
* This use case encapsulates the business logic for age class management
* including validation, duplicate checking, and persistence.
*/
class CreateAltersklasseUseCase(
private val altersklasseRepository: AltersklasseRepository
) {
/**
* Request data for creating a new age class.
*/
data class CreateAltersklasseRequest(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: SparteE? = null,
val geschlechtFilter: Char? = null,
val oetoRegelReferenzId: Uuid? = null,
val istAktiv: Boolean = true
)
/**
* Request data for updating an existing age class.
*/
data class UpdateAltersklasseRequest(
val altersklasseId: Uuid,
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: SparteE? = null,
val geschlechtFilter: Char? = null,
val oetoRegelReferenzId: Uuid? = null,
val istAktiv: Boolean = true
)
/**
* Response data for age class creation.
*/
data class CreateAltersklasseResponse(
val altersklasse: AltersklasseDefinition?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Response data for age class update.
*/
data class UpdateAltersklasseResponse(
val altersklasse: AltersklasseDefinition?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Response data for age class deletion.
*/
data class DeleteAltersklasseResponse(
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Creates a new age class after validation.
*
* @param request The age class creation request
* @return CreateAltersklasseResponse with the created age class or validation errors
*/
suspend fun createAltersklasse(request: CreateAltersklasseRequest): CreateAltersklasseResponse {
// Validate the request
val validationResult = validateCreateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
return CreateAltersklasseResponse(
altersklasse = null,
success = false,
errors = errors
)
}
// Check for duplicates
val duplicateCheck = checkForDuplicates(request.altersklasseCode)
if (!duplicateCheck.isValid()) {
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
return CreateAltersklasseResponse(
altersklasse = null,
success = false,
errors = errors
)
}
// Create the domain object
val now = Clock.System.now()
val altersklasse = AltersklasseDefinition(
altersklasseCode = request.altersklasseCode.trim().uppercase(),
bezeichnung = request.bezeichnung.trim(),
minAlter = request.minAlter,
maxAlter = request.maxAlter,
stichtagRegelText = request.stichtagRegelText?.trim(),
sparteFilter = request.sparteFilter,
geschlechtFilter = request.geschlechtFilter,
oetoRegelReferenzId = request.oetoRegelReferenzId,
istAktiv = request.istAktiv,
createdAt = now,
updatedAt = now
)
// Save to repository
val savedAltersklasse = altersklasseRepository.save(altersklasse)
return CreateAltersklasseResponse(
altersklasse = savedAltersklasse,
success = true
)
}
/**
* Updates an existing age class after validation.
*
* @param request The age class update request
* @return UpdateAltersklasseResponse containing the updated age class or validation errors
*/
suspend fun updateAltersklasse(request: UpdateAltersklasseRequest): UpdateAltersklasseResponse {
// Check if age class exists
val existingAltersklasse = altersklasseRepository.findById(request.altersklasseId)
if (existingAltersklasse == null) {
return UpdateAltersklasseResponse(
altersklasse = null,
success = false,
errors = listOf("Age class with ID ${request.altersklasseId} not found")
)
}
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
return UpdateAltersklasseResponse(
altersklasse = null,
success = false,
errors = errors
)
}
// Check for duplicates (excluding current age class)
val duplicateCheck = checkForDuplicatesExcluding(request.altersklasseCode, request.altersklasseId)
if (!duplicateCheck.isValid()) {
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
return UpdateAltersklasseResponse(
altersklasse = null,
success = false,
errors = errors
)
}
// Update the domain object
val updatedAltersklasse = existingAltersklasse.copy(
altersklasseCode = request.altersklasseCode.trim().uppercase(),
bezeichnung = request.bezeichnung.trim(),
minAlter = request.minAlter,
maxAlter = request.maxAlter,
stichtagRegelText = request.stichtagRegelText?.trim(),
sparteFilter = request.sparteFilter,
geschlechtFilter = request.geschlechtFilter,
oetoRegelReferenzId = request.oetoRegelReferenzId,
istAktiv = request.istAktiv,
updatedAt = Clock.System.now()
)
// Save to repository
val savedAltersklasse = altersklasseRepository.save(updatedAltersklasse)
return UpdateAltersklasseResponse(
altersklasse = savedAltersklasse,
success = true
)
}
/**
* Deletes an age class by ID.
*
* @param altersklasseId The unique identifier of the age class to delete
* @return DeleteAltersklasseResponse indicating success or failure
*/
suspend fun deleteAltersklasse(altersklasseId: Uuid): DeleteAltersklasseResponse {
val deleted = altersklasseRepository.delete(altersklasseId)
return if (deleted) {
DeleteAltersklasseResponse(success = true)
} else {
DeleteAltersklasseResponse(
success = false,
errors = listOf("Age class with ID $altersklasseId not found or could not be deleted")
)
}
}
/**
* Validates a create age class request.
*/
private fun validateCreateRequest(request: CreateAltersklasseRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Age class code validation
if (request.altersklasseCode.isBlank()) {
errors.add(ValidationError("altersklasseCode", "Age class code is required", "REQUIRED"))
} else if (request.altersklasseCode.length > 50) {
errors.add(ValidationError("altersklasseCode", "Age class code must not exceed 50 characters", "MAX_LENGTH"))
} else if (!request.altersklasseCode.matches(Regex("^[A-Z0-9_]+$"))) {
errors.add(ValidationError("altersklasseCode", "Age class code must contain only uppercase letters, numbers, and underscores", "INVALID_FORMAT"))
}
// Bezeichnung validation
if (request.bezeichnung.isBlank()) {
errors.add(ValidationError("bezeichnung", "Bezeichnung is required", "REQUIRED"))
} else if (request.bezeichnung.length > 200) {
errors.add(ValidationError("bezeichnung", "Bezeichnung must not exceed 200 characters", "MAX_LENGTH"))
}
// Age range validation
request.minAlter?.let { min ->
if (min < 0) {
errors.add(ValidationError("minAlter", "Minimum age must be non-negative", "INVALID_VALUE"))
}
}
request.maxAlter?.let { max ->
if (max < 0) {
errors.add(ValidationError("maxAlter", "Maximum age must be non-negative", "INVALID_VALUE"))
}
request.minAlter?.let { min ->
if (max < min) {
errors.add(ValidationError("maxAlter", "Maximum age must be greater than or equal to minimum age", "INVALID_RANGE"))
}
}
}
// Stichtag regel text validation
request.stichtagRegelText?.let { text ->
if (text.length > 500) {
errors.add(ValidationError("stichtagRegelText", "Stichtag regel text must not exceed 500 characters", "MAX_LENGTH"))
}
}
// Gender filter validation
request.geschlechtFilter?.let { gender ->
if (gender != 'M' && gender != 'W') {
errors.add(ValidationError("geschlechtFilter", "Gender filter must be 'M' or 'W'", "INVALID_VALUE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Validates an update age class request.
*/
private fun validateUpdateRequest(request: UpdateAltersklasseRequest): ValidationResult {
// Use the same validation logic as create request
val createRequest = CreateAltersklasseRequest(
altersklasseCode = request.altersklasseCode,
bezeichnung = request.bezeichnung,
minAlter = request.minAlter,
maxAlter = request.maxAlter,
stichtagRegelText = request.stichtagRegelText,
sparteFilter = request.sparteFilter,
geschlechtFilter = request.geschlechtFilter,
oetoRegelReferenzId = request.oetoRegelReferenzId,
istAktiv = request.istAktiv
)
return validateCreateRequest(createRequest)
}
/**
* Checks for duplicate age class codes.
*/
private suspend fun checkForDuplicates(altersklasseCode: String): ValidationResult {
val errors = mutableListOf<ValidationError>()
if (altersklasseRepository.existsByCode(altersklasseCode.trim().uppercase())) {
errors.add(ValidationError("altersklasseCode", "Age class with code '${altersklasseCode.uppercase()}' already exists", "DUPLICATE"))
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Checks for duplicate age class codes excluding a specific age class ID.
*/
private suspend fun checkForDuplicatesExcluding(altersklasseCode: String, excludeId: Uuid): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Check code
val existing = altersklasseRepository.findByCode(altersklasseCode.trim().uppercase())
if (existing != null && existing.altersklasseId != excludeId) {
errors.add(ValidationError("altersklasseCode", "Age class with code '${altersklasseCode.uppercase()}' already exists", "DUPLICATE"))
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Validates age eligibility for a specific age class and participant.
* This is a business logic method that can be used by other parts of the application.
*
* @param altersklasseId The age class ID
* @param participantAge The participant's age
* @param participantGender The participant's gender ('M', 'W')
* @param participantSparte The participant's sport type
* @return ValidationResult indicating eligibility or reasons for ineligibility
*/
suspend fun validateEligibility(
altersklasseId: Uuid,
participantAge: Int,
participantGender: Char,
participantSparte: SparteE
): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Get the age class
val altersklasse = altersklasseRepository.findById(altersklasseId)
if (altersklasse == null) {
errors.add(ValidationError("altersklasseId", "Age class not found", "NOT_FOUND"))
return ValidationResult.Invalid(errors)
}
// Check if age class is active
if (!altersklasse.istAktiv) {
errors.add(ValidationError("altersklasse", "Age class is not active", "INACTIVE"))
}
// Check age eligibility
altersklasse.minAlter?.let { min ->
if (participantAge < min) {
errors.add(ValidationError("age", "Participant is too young for this age class (minimum age: $min)", "AGE_TOO_LOW"))
}
}
altersklasse.maxAlter?.let { max ->
if (participantAge > max) {
errors.add(ValidationError("age", "Participant is too old for this age class (maximum age: $max)", "AGE_TOO_HIGH"))
}
}
// Check gender eligibility
altersklasse.geschlechtFilter?.let { requiredGender ->
if (participantGender != requiredGender) {
val genderName = if (requiredGender == 'M') "male" else "female"
errors.add(ValidationError("gender", "This age class is only for $genderName participants", "GENDER_MISMATCH"))
}
}
// Check sport eligibility
altersklasse.sparteFilter?.let { requiredSparte ->
if (participantSparte != requiredSparte) {
errors.add(ValidationError("sparte", "This age class is only for ${requiredSparte.name} sport", "SPORT_MISMATCH"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
}
@@ -0,0 +1,338 @@
package at.mocode.masterdata.application.usecase
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.masterdata.domain.repository.BundeslandRepository
import at.mocode.core.utils.validation.ValidationResult
import at.mocode.core.utils.validation.ValidationError
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
/**
* Use case for creating and updating federal state information.
*
* This use case encapsulates the business logic for federal state management
* including validation, duplicate checking, and persistence.
*/
class CreateBundeslandUseCase(
private val bundeslandRepository: BundeslandRepository
) {
/**
* Request data for creating a new federal state.
*/
data class CreateBundeslandRequest(
val landId: Uuid,
val oepsCode: String? = null,
val iso3166_2_Code: String? = null,
val name: String,
val kuerzel: String? = null,
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Request data for updating an existing federal state.
*/
data class UpdateBundeslandRequest(
val bundeslandId: Uuid,
val landId: Uuid,
val oepsCode: String? = null,
val iso3166_2_Code: String? = null,
val name: String,
val kuerzel: String? = null,
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Response data for federal state creation.
*/
data class CreateBundeslandResponse(
val bundesland: BundeslandDefinition?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Response data for federal state update.
*/
data class UpdateBundeslandResponse(
val bundesland: BundeslandDefinition?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Response data for federal state deletion.
*/
data class DeleteBundeslandResponse(
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Creates a new federal state after validation.
*
* @param request The federal state creation request
* @return CreateBundeslandResponse with the created federal state or validation errors
*/
suspend fun createBundesland(request: CreateBundeslandRequest): CreateBundeslandResponse {
// Validate the request
val validationResult = validateCreateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
return CreateBundeslandResponse(
bundesland = null,
success = false,
errors = errors
)
}
// Check for duplicates
val duplicateCheck = checkForDuplicates(request.oepsCode, request.iso3166_2_Code, request.landId)
if (!duplicateCheck.isValid()) {
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
return CreateBundeslandResponse(
bundesland = null,
success = false,
errors = errors
)
}
// Create the domain object
val now = Clock.System.now()
val bundesland = BundeslandDefinition(
landId = request.landId,
oepsCode = request.oepsCode?.trim(),
iso3166_2_Code = request.iso3166_2_Code?.trim()?.uppercase(),
name = request.name.trim(),
kuerzel = request.kuerzel?.trim(),
wappenUrl = request.wappenUrl?.trim(),
istAktiv = request.istAktiv,
sortierReihenfolge = request.sortierReihenfolge,
createdAt = now,
updatedAt = now
)
// Save to repository
val savedBundesland = bundeslandRepository.save(bundesland)
return CreateBundeslandResponse(
bundesland = savedBundesland,
success = true
)
}
/**
* Updates an existing federal state after validation.
*
* @param request The federal state update request
* @return UpdateBundeslandResponse containing the updated federal state or validation errors
*/
suspend fun updateBundesland(request: UpdateBundeslandRequest): UpdateBundeslandResponse {
// Check if federal state exists
val existingBundesland = bundeslandRepository.findById(request.bundeslandId)
if (existingBundesland == null) {
return UpdateBundeslandResponse(
bundesland = null,
success = false,
errors = listOf("Federal state with ID ${request.bundeslandId} not found")
)
}
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
return UpdateBundeslandResponse(
bundesland = null,
success = false,
errors = errors
)
}
// Check for duplicates (excluding current federal state)
val duplicateCheck = checkForDuplicatesExcluding(
request.oepsCode,
request.iso3166_2_Code,
request.landId,
request.bundeslandId
)
if (!duplicateCheck.isValid()) {
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
return UpdateBundeslandResponse(
bundesland = null,
success = false,
errors = errors
)
}
// Update the domain object
val updatedBundesland = existingBundesland.copy(
landId = request.landId,
oepsCode = request.oepsCode?.trim(),
iso3166_2_Code = request.iso3166_2_Code?.trim()?.uppercase(),
name = request.name.trim(),
kuerzel = request.kuerzel?.trim(),
wappenUrl = request.wappenUrl?.trim(),
istAktiv = request.istAktiv,
sortierReihenfolge = request.sortierReihenfolge,
updatedAt = Clock.System.now()
)
// Save to repository
val savedBundesland = bundeslandRepository.save(updatedBundesland)
return UpdateBundeslandResponse(
bundesland = savedBundesland,
success = true
)
}
/**
* Deletes a federal state by ID.
*
* @param bundeslandId The unique identifier of the federal state to delete
* @return DeleteBundeslandResponse indicating success or failure
*/
suspend fun deleteBundesland(bundeslandId: Uuid): DeleteBundeslandResponse {
val deleted = bundeslandRepository.delete(bundeslandId)
return if (deleted) {
DeleteBundeslandResponse(success = true)
} else {
DeleteBundeslandResponse(
success = false,
errors = listOf("Federal state with ID $bundeslandId not found or could not be deleted")
)
}
}
/**
* Validates a create federal state request.
*/
private fun validateCreateRequest(request: CreateBundeslandRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Name validation
if (request.name.isBlank()) {
errors.add(ValidationError("name", "Name is required", "REQUIRED"))
} else if (request.name.length > 100) {
errors.add(ValidationError("name", "Name must not exceed 100 characters", "MAX_LENGTH"))
}
// OEPS code validation
request.oepsCode?.let { code ->
if (code.isBlank()) {
errors.add(ValidationError("oepsCode", "OEPS code cannot be empty if provided", "INVALID_FORMAT"))
} else if (code.length > 10) {
errors.add(ValidationError("oepsCode", "OEPS code must not exceed 10 characters", "MAX_LENGTH"))
}
}
// ISO 3166-2 code validation
request.iso3166_2_Code?.let { code ->
if (code.isBlank()) {
errors.add(ValidationError("iso3166_2_Code", "ISO 3166-2 code cannot be empty if provided", "INVALID_FORMAT"))
} else if (code.length > 10) {
errors.add(ValidationError("iso3166_2_Code", "ISO 3166-2 code must not exceed 10 characters", "MAX_LENGTH"))
}
}
// Kuerzel validation
request.kuerzel?.let { kuerzel ->
if (kuerzel.length > 10) {
errors.add(ValidationError("kuerzel", "Kuerzel must not exceed 10 characters", "MAX_LENGTH"))
}
}
// Sorting order validation
request.sortierReihenfolge?.let { order ->
if (order < 0) {
errors.add(ValidationError("sortierReihenfolge", "Sorting order must be non-negative", "INVALID_VALUE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Validates an update federal state request.
*/
private fun validateUpdateRequest(request: UpdateBundeslandRequest): ValidationResult {
// Use the same validation logic as create request
val createRequest = CreateBundeslandRequest(
landId = request.landId,
oepsCode = request.oepsCode,
iso3166_2_Code = request.iso3166_2_Code,
name = request.name,
kuerzel = request.kuerzel,
wappenUrl = request.wappenUrl,
istAktiv = request.istAktiv,
sortierReihenfolge = request.sortierReihenfolge
)
return validateCreateRequest(createRequest)
}
/**
* Checks for duplicate codes.
*/
private suspend fun checkForDuplicates(oepsCode: String?, iso3166_2_Code: String?, landId: Uuid): ValidationResult {
val errors = mutableListOf<ValidationError>()
oepsCode?.let { code ->
if (bundeslandRepository.existsByOepsCode(code.trim(), landId)) {
errors.add(ValidationError("oepsCode", "Federal state with OEPS code '$code' already exists for this country", "DUPLICATE"))
}
}
iso3166_2_Code?.let { code ->
if (bundeslandRepository.existsByIso3166_2_Code(code.trim().uppercase())) {
errors.add(ValidationError("iso3166_2_Code", "Federal state with ISO 3166-2 code '${code.uppercase()}' already exists", "DUPLICATE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Checks for duplicate codes excluding a specific federal state ID.
*/
private suspend fun checkForDuplicatesExcluding(
oepsCode: String?,
iso3166_2_Code: String?,
landId: Uuid,
excludeId: Uuid
): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Check OEPS code
oepsCode?.let { code ->
val existing = bundeslandRepository.findByOepsCode(code.trim(), landId)
if (existing != null && existing.bundeslandId != excludeId) {
errors.add(ValidationError("oepsCode", "Federal state with OEPS code '$code' already exists for this country", "DUPLICATE"))
}
}
// Check ISO 3166-2 code
iso3166_2_Code?.let { code ->
val existing = bundeslandRepository.findByIso3166_2_Code(code.trim().uppercase())
if (existing != null && existing.bundeslandId != excludeId) {
errors.add(ValidationError("iso3166_2_Code", "Federal state with ISO 3166-2 code '${code.uppercase()}' already exists", "DUPLICATE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
}
@@ -0,0 +1,455 @@
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.masterdata.domain.model.Platz
import at.mocode.masterdata.domain.repository.PlatzRepository
import at.mocode.core.utils.validation.ValidationResult
import at.mocode.core.utils.validation.ValidationError
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
/**
* Use case for creating and updating venue/arena information.
*
* This use case encapsulates the business logic for venue management
* including validation, duplicate checking, and persistence.
*/
class CreatePlatzUseCase(
private val platzRepository: PlatzRepository
) {
/**
* Request data for creating a new venue.
*/
data class CreatePlatzRequest(
val turnierId: Uuid,
val name: String,
val dimension: String? = null,
val boden: String? = null,
val typ: PlatzTypE,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Request data for updating an existing venue.
*/
data class UpdatePlatzRequest(
val platzId: Uuid,
val turnierId: Uuid,
val name: String,
val dimension: String? = null,
val boden: String? = null,
val typ: PlatzTypE,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Response data for venue creation.
*/
data class CreatePlatzResponse(
val platz: Platz?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Response data for venue update.
*/
data class UpdatePlatzResponse(
val platz: Platz?,
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Response data for venue deletion.
*/
data class DeletePlatzResponse(
val success: Boolean,
val errors: List<String> = emptyList()
)
/**
* Creates a new venue after validation.
*
* @param request The venue creation request
* @return CreatePlatzResponse with the created venue or validation errors
*/
suspend fun createPlatz(request: CreatePlatzRequest): CreatePlatzResponse {
// Validate the request
val validationResult = validateCreateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
return CreatePlatzResponse(
platz = null,
success = false,
errors = errors
)
}
// Check for duplicates
val duplicateCheck = checkForDuplicates(request.name, request.turnierId)
if (!duplicateCheck.isValid()) {
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
return CreatePlatzResponse(
platz = null,
success = false,
errors = errors
)
}
// Create the domain object
val now = Clock.System.now()
val platz = Platz(
turnierId = request.turnierId,
name = request.name.trim(),
dimension = request.dimension?.trim(),
boden = request.boden?.trim(),
typ = request.typ,
istAktiv = request.istAktiv,
sortierReihenfolge = request.sortierReihenfolge,
createdAt = now,
updatedAt = now
)
// Save to repository
val savedPlatz = platzRepository.save(platz)
return CreatePlatzResponse(
platz = savedPlatz,
success = true
)
}
/**
* Updates an existing venue after validation.
*
* @param request The venue update request
* @return UpdatePlatzResponse containing the updated venue or validation errors
*/
suspend fun updatePlatz(request: UpdatePlatzRequest): UpdatePlatzResponse {
// Check if venue exists
val existingPlatz = platzRepository.findById(request.platzId)
if (existingPlatz == null) {
return UpdatePlatzResponse(
platz = null,
success = false,
errors = listOf("Venue with ID ${request.platzId} not found")
)
}
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
return UpdatePlatzResponse(
platz = null,
success = false,
errors = errors
)
}
// Check for duplicates (excluding current venue)
val duplicateCheck = checkForDuplicatesExcluding(request.name, request.turnierId, request.platzId)
if (!duplicateCheck.isValid()) {
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
return UpdatePlatzResponse(
platz = null,
success = false,
errors = errors
)
}
// Update the domain object
val updatedPlatz = existingPlatz.copy(
turnierId = request.turnierId,
name = request.name.trim(),
dimension = request.dimension?.trim(),
boden = request.boden?.trim(),
typ = request.typ,
istAktiv = request.istAktiv,
sortierReihenfolge = request.sortierReihenfolge,
updatedAt = Clock.System.now()
)
// Save to repository
val savedPlatz = platzRepository.save(updatedPlatz)
return UpdatePlatzResponse(
platz = savedPlatz,
success = true
)
}
/**
* Deletes a venue by ID.
*
* @param platzId The unique identifier of the venue to delete
* @return DeletePlatzResponse indicating success or failure
*/
suspend fun deletePlatz(platzId: Uuid): DeletePlatzResponse {
val deleted = platzRepository.delete(platzId)
return if (deleted) {
DeletePlatzResponse(success = true)
} else {
DeletePlatzResponse(
success = false,
errors = listOf("Venue with ID $platzId not found or could not be deleted")
)
}
}
/**
* Validates a create venue request.
*/
private fun validateCreateRequest(request: CreatePlatzRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Name validation
if (request.name.isBlank()) {
errors.add(ValidationError("name", "Name is required", "REQUIRED"))
} else if (request.name.length > 200) {
errors.add(ValidationError("name", "Name must not exceed 200 characters", "MAX_LENGTH"))
}
// Dimension validation
request.dimension?.let { dimension ->
if (dimension.isBlank()) {
errors.add(ValidationError("dimension", "Dimension cannot be empty if provided", "INVALID_FORMAT"))
} else if (dimension.length > 50) {
errors.add(ValidationError("dimension", "Dimension must not exceed 50 characters", "MAX_LENGTH"))
} else if (!dimension.matches(Regex("^\\d+x\\d+m?$"))) {
errors.add(ValidationError("dimension", "Dimension must be in format like '20x60m' or '20x40'", "INVALID_FORMAT"))
}
}
// Ground type validation
request.boden?.let { boden ->
if (boden.isBlank()) {
errors.add(ValidationError("boden", "Ground type cannot be empty if provided", "INVALID_FORMAT"))
} else if (boden.length > 100) {
errors.add(ValidationError("boden", "Ground type must not exceed 100 characters", "MAX_LENGTH"))
}
}
// Sorting order validation
request.sortierReihenfolge?.let { order ->
if (order < 0) {
errors.add(ValidationError("sortierReihenfolge", "Sorting order must be non-negative", "INVALID_VALUE"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Validates an update venue request.
*/
private fun validateUpdateRequest(request: UpdatePlatzRequest): ValidationResult {
// Use the same validation logic as create request
val createRequest = CreatePlatzRequest(
turnierId = request.turnierId,
name = request.name,
dimension = request.dimension,
boden = request.boden,
typ = request.typ,
istAktiv = request.istAktiv,
sortierReihenfolge = request.sortierReihenfolge
)
return validateCreateRequest(createRequest)
}
/**
* Checks for duplicate venue names within a tournament.
*/
private suspend fun checkForDuplicates(name: String, turnierId: Uuid): ValidationResult {
val errors = mutableListOf<ValidationError>()
if (platzRepository.existsByNameAndTournament(name.trim(), turnierId)) {
errors.add(ValidationError("name", "Venue with name '$name' already exists for this tournament", "DUPLICATE"))
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Checks for duplicate venue names excluding a specific venue ID.
*/
private suspend fun checkForDuplicatesExcluding(name: String, turnierId: Uuid, excludeId: Uuid): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Get all venues with the same name and tournament
val existingVenues = platzRepository.findByName(name.trim(), turnierId, 10)
val duplicateExists = existingVenues.any { it.id != excludeId }
if (duplicateExists) {
errors.add(ValidationError("name", "Venue with name '$name' already exists for this tournament", "DUPLICATE"))
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Validates venue configuration for specific discipline requirements.
* This is a business logic method that can be used by other parts of the application.
*
* @param platzId The venue ID
* @param requiredType The required venue type for the discipline
* @param requiredDimensions Optional required dimensions
* @param requiredGroundType Optional required ground type
* @return ValidationResult indicating suitability or reasons for unsuitability
*/
suspend fun validateVenueForDiscipline(
platzId: Uuid,
requiredType: PlatzTypE,
requiredDimensions: String? = null,
requiredGroundType: String? = null
): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Get the venue
val platz = platzRepository.findById(platzId)
if (platz == null) {
errors.add(ValidationError("platzId", "Venue not found", "NOT_FOUND"))
return ValidationResult.Invalid(errors)
}
// Check if venue is active
if (!platz.istAktiv) {
errors.add(ValidationError("platz", "Venue is not active", "INACTIVE"))
}
// Check venue type
if (platz.typ != requiredType) {
errors.add(ValidationError("typ", "Venue type ${platz.typ} does not match required type $requiredType", "TYPE_MISMATCH"))
}
// Check dimensions if required
requiredDimensions?.let { required ->
if (platz.dimension != required.trim()) {
errors.add(ValidationError("dimension", "Venue dimensions '${platz.dimension}' do not match required dimensions '$required'", "DIMENSION_MISMATCH"))
}
}
// Check ground type if required
requiredGroundType?.let { required ->
if (platz.boden != required.trim()) {
errors.add(ValidationError("boden", "Venue ground type '${platz.boden}' does not match required ground type '$required'", "GROUND_TYPE_MISMATCH"))
}
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Creates multiple venues for a tournament in batch.
* This is a convenience method for setting up tournament venues efficiently.
*
* @param turnierId The tournament ID
* @param venueRequests List of venue creation requests
* @return List of creation responses for each venue
*/
suspend fun createMultipleVenues(turnierId: Uuid, venueRequests: List<CreatePlatzRequest>): List<CreatePlatzResponse> {
val responses = mutableListOf<CreatePlatzResponse>()
for (request in venueRequests) {
// Ensure all requests are for the same tournament
val adjustedRequest = request.copy(turnierId = turnierId)
val response = createPlatz(adjustedRequest)
responses.add(response)
}
return responses
}
/**
* Validates venue capacity and setup for tournament requirements.
* This method performs comprehensive checks for tournament venue setup.
*
* @param turnierId The tournament ID
* @param requiredVenueTypes Map of venue type to minimum count required
* @return ValidationResult indicating if the tournament has adequate venue setup
*/
suspend fun validateTournamentVenueSetup(
turnierId: Uuid,
requiredVenueTypes: Map<PlatzTypE, Int>
): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Get all active venues for the tournament
val venues = platzRepository.findByTournament(turnierId, activeOnly = true, orderBySortierung = false)
val venuesByType = venues.groupBy { it.typ }
// Check if each required venue type has sufficient count
for ((requiredType, requiredCount) in requiredVenueTypes) {
val availableCount = venuesByType[requiredType]?.size ?: 0
if (availableCount < requiredCount) {
errors.add(ValidationError(
"venues",
"Tournament requires $requiredCount venues of type $requiredType but only has $availableCount",
"INSUFFICIENT_VENUES"
))
}
}
// Check if tournament has any venues at all
if (venues.isEmpty()) {
errors.add(ValidationError("venues", "Tournament has no active venues configured", "NO_VENUES"))
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
/**
* Optimizes venue sorting order for a tournament.
* This method automatically assigns sorting orders based on venue type and name.
*
* @param turnierId The tournament ID
* @return Number of venues updated
*/
suspend fun optimizeVenueSorting(turnierId: Uuid): Int {
val venues = platzRepository.findByTournament(turnierId, activeOnly = false, orderBySortierung = false)
// Sort venues by type first, then by name
val sortedVenues = venues.sortedWith(compareBy<Platz> { it.typ.ordinal }.thenBy { it.name })
var updatedCount = 0
// Assign new sorting orders
sortedVenues.forEachIndexed { index, venue ->
val newSortOrder = (index + 1) * 10 // Leave gaps for future insertions
if (venue.sortierReihenfolge != newSortOrder) {
val updatedVenue = venue.copy(
sortierReihenfolge = newSortOrder,
updatedAt = Clock.System.now()
)
platzRepository.save(updatedVenue)
updatedCount++
}
}
return updatedCount
}
}
@@ -0,0 +1,185 @@
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.masterdata.domain.repository.AltersklasseRepository
import com.benasher44.uuid.Uuid
/**
* Use case for retrieving age class information.
*
* This use case encapsulates the business logic for fetching age class data
* and provides a clean interface for the application layer.
*/
class GetAltersklasseUseCase(
private val altersklasseRepository: AltersklasseRepository
) {
/**
* Retrieves an age class by its unique ID.
*
* @param altersklasseId The unique identifier of the age class
* @return The age class if found, null otherwise
*/
suspend fun getById(altersklasseId: Uuid): AltersklasseDefinition? {
return altersklasseRepository.findById(altersklasseId)
}
/**
* Retrieves an age class by its code.
*
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
* @return The age class if found, null otherwise
*/
suspend fun getByCode(altersklasseCode: String): AltersklasseDefinition? {
require(altersklasseCode.isNotBlank()) { "Age class code cannot be blank" }
return altersklasseRepository.findByCode(altersklasseCode.trim().uppercase())
}
/**
* Searches for age classes by name (partial match).
*
* @param searchTerm The search term to match against age class names
* @param limit Maximum number of results to return (default: 50)
* @return List of matching age classes
*/
suspend fun searchByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition> {
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
require(limit > 0) { "Limit must be positive" }
return altersklasseRepository.findByName(searchTerm.trim(), limit)
}
/**
* Retrieves all active age classes.
*
* @param sparteFilter Optional filter by sport type
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of active age classes
*/
suspend fun getAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition> {
geschlechtFilter?.let { gender ->
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
}
return altersklasseRepository.findAllActive(sparteFilter, geschlechtFilter)
}
/**
* Finds age classes applicable for a specific age.
*
* @param age The age to check
* @param sparteFilter Optional filter by sport type
* @param geschlechtFilter Optional filter by gender ('M', 'W')
* @return List of applicable age classes
*/
suspend fun getApplicableForAge(age: Int, sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition> {
require(age >= 0) { "Age must be non-negative" }
geschlechtFilter?.let { gender ->
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
}
return altersklasseRepository.findApplicableForAge(age, sparteFilter, geschlechtFilter)
}
/**
* Retrieves age classes by sport type.
*
* @param sparte The sport type
* @param activeOnly Whether to return only active age classes (default: true)
* @return List of age classes for the sport type
*/
suspend fun getBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition> {
return altersklasseRepository.findBySparte(sparte, activeOnly)
}
/**
* Retrieves age classes by gender filter.
*
* @param geschlecht The gender ('M', 'W')
* @param activeOnly Whether to return only active age classes (default: true)
* @return List of age classes for the gender
*/
suspend fun getByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition> {
require(geschlecht == 'M' || geschlecht == 'W') { "Gender must be 'M' or 'W'" }
return altersklasseRepository.findByGeschlecht(geschlecht, activeOnly)
}
/**
* Retrieves age classes by age range.
*
* @param minAge Minimum age (inclusive)
* @param maxAge Maximum age (inclusive)
* @param activeOnly Whether to return only active age classes (default: true)
* @return List of age classes within the age range
*/
suspend fun getByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition> {
minAge?.let { min ->
require(min >= 0) { "Minimum age must be non-negative" }
}
maxAge?.let { max ->
require(max >= 0) { "Maximum age must be non-negative" }
minAge?.let { min ->
require(max >= min) { "Maximum age must be greater than or equal to minimum age" }
}
}
return altersklasseRepository.findByAgeRange(minAge, maxAge, activeOnly)
}
/**
* Retrieves age classes by OETO rule reference.
*
* @param oetoRegelReferenzId The OETO rule reference ID
* @return List of age classes linked to the rule
*/
suspend fun getByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> {
return altersklasseRepository.findByOetoRegelReferenz(oetoRegelReferenzId)
}
/**
* Checks if an age class with the given code exists.
*
* @param altersklasseCode The age class code to check
* @return true if an age class with this code exists, false otherwise
*/
suspend fun existsByCode(altersklasseCode: String): Boolean {
require(altersklasseCode.isNotBlank()) { "Age class code cannot be blank" }
return altersklasseRepository.existsByCode(altersklasseCode.trim().uppercase())
}
/**
* Counts the total number of active age classes.
*
* @param sparteFilter Optional filter by sport type
* @return The total count of active age classes
*/
suspend fun countActive(sparteFilter: SparteE? = null): Long {
return altersklasseRepository.countActive(sparteFilter)
}
/**
* Validates if a person with given age and gender can participate in an age class.
*
* @param altersklasseId The age class ID
* @param age The person's age
* @param geschlecht The person's gender ('M', 'W')
* @return true if the person can participate, false otherwise
*/
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean {
require(age >= 0) { "Age must be non-negative" }
require(geschlecht == 'M' || geschlecht == 'W') { "Gender must be 'M' or 'W'" }
return altersklasseRepository.isEligible(altersklasseId, age, geschlecht)
}
/**
* Retrieves age classes suitable for a participant based on age, gender, and sport.
* This is a convenience method that combines multiple filters.
*
* @param age The participant's age
* @param geschlecht The participant's gender ('M', 'W')
* @param sparte The sport type
* @return List of suitable age classes
*/
suspend fun getSuitableForParticipant(age: Int, geschlecht: Char, sparte: SparteE): List<AltersklasseDefinition> {
require(age >= 0) { "Age must be non-negative" }
require(geschlecht == 'M' || geschlecht == 'W') { "Gender must be 'M' or 'W'" }
return altersklasseRepository.findApplicableForAge(age, sparte, geschlecht)
}
}
@@ -0,0 +1,118 @@
package at.mocode.masterdata.application.usecase
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.masterdata.domain.repository.BundeslandRepository
import com.benasher44.uuid.Uuid
/**
* Use case for retrieving federal state information.
*
* This use case encapsulates the business logic for fetching federal state data
* and provides a clean interface for the application layer.
*/
class GetBundeslandUseCase(
private val bundeslandRepository: BundeslandRepository
) {
/**
* Retrieves a federal state by its unique ID.
*
* @param bundeslandId The unique identifier of the federal state
* @return The federal state if found, null otherwise
*/
suspend fun getById(bundeslandId: Uuid): BundeslandDefinition? {
return bundeslandRepository.findById(bundeslandId)
}
/**
* Retrieves a federal state by its OEPS code for a specific country.
*
* @param oepsCode The OEPS code (e.g., "01", "02")
* @param landId The country ID
* @return The federal state if found, null otherwise
*/
suspend fun getByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? {
require(oepsCode.isNotBlank()) { "OEPS code cannot be blank" }
return bundeslandRepository.findByOepsCode(oepsCode.trim(), landId)
}
/**
* Retrieves a federal state by its ISO 3166-2 code.
*
* @param iso3166_2_Code The ISO 3166-2 code (e.g., "AT-1", "DE-BY")
* @return The federal state if found, null otherwise
*/
suspend fun getByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? {
require(iso3166_2_Code.isNotBlank()) { "ISO 3166-2 code cannot be blank" }
return bundeslandRepository.findByIso3166_2_Code(iso3166_2_Code.trim().uppercase())
}
/**
* Retrieves all federal states for a specific country.
*
* @param landId The country ID
* @param activeOnly Whether to return only active federal states (default: true)
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of federal states for the country
*/
suspend fun getByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<BundeslandDefinition> {
return bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
}
/**
* Searches for federal states by name (partial match).
*
* @param searchTerm The search term to match against federal state names
* @param landId Optional country ID to limit search
* @param limit Maximum number of results to return (default: 50)
* @return List of matching federal states
*/
suspend fun searchByName(searchTerm: String, landId: Uuid? = null, limit: Int = 50): List<BundeslandDefinition> {
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
require(limit > 0) { "Limit must be positive" }
return bundeslandRepository.findByName(searchTerm.trim(), landId, limit)
}
/**
* Retrieves all active federal states.
*
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of active federal states
*/
suspend fun getAllActive(orderBySortierung: Boolean = true): List<BundeslandDefinition> {
return bundeslandRepository.findAllActive(orderBySortierung)
}
/**
* Checks if a federal state with the given OEPS code exists for a country.
*
* @param oepsCode The OEPS code to check
* @param landId The country ID
* @return true if a federal state with this code exists, false otherwise
*/
suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean {
require(oepsCode.isNotBlank()) { "OEPS code cannot be blank" }
return bundeslandRepository.existsByOepsCode(oepsCode.trim(), landId)
}
/**
* Checks if a federal state with the given ISO 3166-2 code exists.
*
* @param iso3166_2_Code The ISO 3166-2 code to check
* @return true if a federal state with this code exists, false otherwise
*/
suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean {
require(iso3166_2_Code.isNotBlank()) { "ISO 3166-2 code cannot be blank" }
return bundeslandRepository.existsByIso3166_2_Code(iso3166_2_Code.trim().uppercase())
}
/**
* Counts the total number of active federal states for a country.
*
* @param landId The country ID
* @return The total count of active federal states
*/
suspend fun countActiveByCountry(landId: Uuid): Long {
return bundeslandRepository.countActiveByCountry(landId)
}
}
@@ -0,0 +1,275 @@
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.masterdata.domain.model.Platz
import at.mocode.masterdata.domain.repository.PlatzRepository
import com.benasher44.uuid.Uuid
/**
* Use case for retrieving venue/arena information.
*
* This use case encapsulates the business logic for fetching venue data
* and provides a clean interface for the application layer.
*/
class GetPlatzUseCase(
private val platzRepository: PlatzRepository
) {
/**
* Retrieves a venue by its unique ID.
*
* @param platzId The unique identifier of the venue
* @return The venue if found, null otherwise
*/
suspend fun getById(platzId: Uuid): Platz? {
return platzRepository.findById(platzId)
}
/**
* Retrieves all venues for a specific tournament.
*
* @param turnierId The tournament ID
* @param activeOnly Whether to return only active venues (default: true)
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of venues for the tournament
*/
suspend fun getByTournament(turnierId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<Platz> {
return platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
}
/**
* Searches for venues by name (partial match).
*
* @param searchTerm The search term to match against venue names
* @param turnierId Optional tournament ID to limit search
* @param limit Maximum number of results to return (default: 50)
* @return List of matching venues
*/
suspend fun searchByName(searchTerm: String, turnierId: Uuid? = null, limit: Int = 50): List<Platz> {
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
require(limit > 0) { "Limit must be positive" }
return platzRepository.findByName(searchTerm.trim(), turnierId, limit)
}
/**
* Retrieves venues by type.
*
* @param typ The venue type
* @param turnierId Optional tournament ID to limit search
* @param activeOnly Whether to return only active venues (default: true)
* @return List of venues of the specified type
*/
suspend fun getByType(typ: PlatzTypE, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz> {
return platzRepository.findByType(typ, turnierId, activeOnly)
}
/**
* Retrieves venues by ground type.
*
* @param boden The ground type (e.g., "Sand", "Gras", "Kunststoff")
* @param turnierId Optional tournament ID to limit search
* @param activeOnly Whether to return only active venues (default: true)
* @return List of venues with the specified ground type
*/
suspend fun getByGroundType(boden: String, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz> {
require(boden.isNotBlank()) { "Ground type cannot be blank" }
return platzRepository.findByGroundType(boden.trim(), turnierId, activeOnly)
}
/**
* Retrieves venues by dimensions.
*
* @param dimension The venue dimensions (e.g., "20x60m", "20x40m")
* @param turnierId Optional tournament ID to limit search
* @param activeOnly Whether to return only active venues (default: true)
* @return List of venues with the specified dimensions
*/
suspend fun getByDimensions(dimension: String, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz> {
require(dimension.isNotBlank()) { "Dimension cannot be blank" }
return platzRepository.findByDimensions(dimension.trim(), turnierId, activeOnly)
}
/**
* Retrieves all active venues.
*
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
* @return List of active venues
*/
suspend fun getAllActive(orderBySortierung: Boolean = true): List<Platz> {
return platzRepository.findAllActive(orderBySortierung)
}
/**
* Finds venues suitable for a specific discipline based on type and dimensions.
*
* @param requiredType The required venue type
* @param requiredDimensions Optional required dimensions
* @param turnierId Optional tournament ID to limit search
* @return List of suitable venues
*/
suspend fun getSuitableForDiscipline(
requiredType: PlatzTypE,
requiredDimensions: String? = null,
turnierId: Uuid? = null
): List<Platz> {
requiredDimensions?.let { dimensions ->
require(dimensions.isNotBlank()) { "Required dimensions cannot be blank if provided" }
}
return platzRepository.findSuitableForDiscipline(requiredType, requiredDimensions?.trim(), turnierId)
}
/**
* Checks if a venue with the given name exists for a tournament.
*
* @param name The venue name to check
* @param turnierId The tournament ID
* @return true if a venue with this name exists, false otherwise
*/
suspend fun existsByNameAndTournament(name: String, turnierId: Uuid): Boolean {
require(name.isNotBlank()) { "Venue name cannot be blank" }
return platzRepository.existsByNameAndTournament(name.trim(), turnierId)
}
/**
* Counts the total number of active venues for a tournament.
*
* @param turnierId The tournament ID
* @return The total count of active venues
*/
suspend fun countActiveByTournament(turnierId: Uuid): Long {
return platzRepository.countActiveByTournament(turnierId)
}
/**
* Counts venues by type for a tournament.
*
* @param typ The venue type
* @param turnierId The tournament ID
* @param activeOnly Whether to count only active venues (default: true)
* @return The count of venues of the specified type
*/
suspend fun countByTypeAndTournament(typ: PlatzTypE, turnierId: Uuid, activeOnly: Boolean = true): Long {
return platzRepository.countByTypeAndTournament(typ, turnierId, activeOnly)
}
/**
* Finds available venues for a specific time slot.
* This method can be extended when venue scheduling functionality is added.
*
* @param turnierId The tournament ID
* @param startTime The start time (placeholder for future scheduling feature)
* @param endTime The end time (placeholder for future scheduling feature)
* @return List of available venues (currently returns all active venues)
*/
suspend fun getAvailableForTimeSlot(turnierId: Uuid, startTime: String? = null, endTime: String? = null): List<Platz> {
return platzRepository.findAvailableForTimeSlot(turnierId, startTime, endTime)
}
/**
* Retrieves venues grouped by type for a tournament.
* This is a convenience method that provides venues organized by their type.
*
* @param turnierId The tournament ID
* @param activeOnly Whether to include only active venues (default: true)
* @return Map of venue type to list of venues
*/
suspend fun getGroupedByTypeForTournament(turnierId: Uuid, activeOnly: Boolean = true): Map<PlatzTypE, List<Platz>> {
val venues = platzRepository.findByTournament(turnierId, activeOnly, true)
return venues.groupBy { it.typ }
}
/**
* Retrieves venues with specific characteristics for discipline matching.
* This method combines multiple filters to find venues suitable for specific disciplines.
*
* @param turnierId The tournament ID
* @param requiredType The required venue type
* @param preferredDimensions Preferred dimensions (optional)
* @param preferredGroundType Preferred ground type (optional)
* @param activeOnly Whether to include only active venues (default: true)
* @return List of venues matching the criteria, sorted by preference
*/
suspend fun getForDisciplineRequirements(
turnierId: Uuid,
requiredType: PlatzTypE,
preferredDimensions: String? = null,
preferredGroundType: String? = null,
activeOnly: Boolean = true
): List<Platz> {
// Start with venues of the required type
val typeMatches = platzRepository.findByType(requiredType, turnierId, activeOnly)
// If no specific preferences, return all type matches
if (preferredDimensions == null && preferredGroundType == null) {
return typeMatches
}
// Filter and sort by preferences
val exactMatches = mutableListOf<Platz>()
val partialMatches = mutableListOf<Platz>()
val otherMatches = mutableListOf<Platz>()
for (venue in typeMatches) {
val dimensionMatch = preferredDimensions == null || venue.dimension == preferredDimensions.trim()
val groundMatch = preferredGroundType == null || venue.boden == preferredGroundType.trim()
when {
dimensionMatch && groundMatch -> exactMatches.add(venue)
dimensionMatch || groundMatch -> partialMatches.add(venue)
else -> otherMatches.add(venue)
}
}
// Return sorted by preference: exact matches first, then partial, then others
return exactMatches + partialMatches + otherMatches
}
/**
* Validates venue availability and suitability for a specific use case.
* This method performs comprehensive checks for venue usage.
*
* @param platzId The venue ID
* @param requiredType Optional required venue type
* @param requiredDimensions Optional required dimensions
* @param requiredGroundType Optional required ground type
* @return Pair of (isValid, reasons) where reasons contains any validation issues
*/
suspend fun validateVenueSuitability(
platzId: Uuid,
requiredType: PlatzTypE? = null,
requiredDimensions: String? = null,
requiredGroundType: String? = null
): Pair<Boolean, List<String>> {
val venue = platzRepository.findById(platzId)
val issues = mutableListOf<String>()
if (venue == null) {
issues.add("Venue not found")
return Pair(false, issues)
}
if (!venue.istAktiv) {
issues.add("Venue is not active")
}
requiredType?.let { type ->
if (venue.typ != type) {
issues.add("Venue type ${venue.typ} does not match required type $type")
}
}
requiredDimensions?.let { dimensions ->
if (venue.dimension != dimensions.trim()) {
issues.add("Venue dimensions '${venue.dimension}' do not match required dimensions '$dimensions'")
}
}
requiredGroundType?.let { groundType ->
if (venue.boden != groundType.trim()) {
issues.add("Venue ground type '${venue.boden}' does not match required ground type '$groundType'")
}
}
return Pair(issues.isEmpty(), issues)
}
}