feat(masterdata): introduce Regulation domain with API, persistence, and metrics integration

- Added `RegulationRepository` and its `Exposed` implementation for persistence.
- Implemented REST endpoints for regulations (`/rules`) in `RegulationController`, including support for tournament classes, license matrix, guidelines, fees, and configuration retrieval.
- Integrated OpenAPI documentation for `/rules` endpoints with Swagger UI in `masterdataApiModule`.
- Enabled Micrometer-based metrics for Prometheus in the API layer.
- Updated Gradle dependencies to include OpenAPI, Swagger, and Micrometer libraries.
- Registered `RegulationRepository` and `RegulationController` in `MasterdataConfiguration`.
- Improved database access patterns and reduced repetitive validation logic across domain services.
- Added unit and application tests for `RegulationController` to verify API behavior and repository interactions.
- Updated the service's `ROADMAP.md` to mark API v1 endpoints and observability as complete.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-30 15:38:13 +02:00
parent d8c9d11adb
commit 2f17778df6
29 changed files with 591 additions and 105 deletions
@@ -2,17 +2,17 @@
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.SparteE
import at.mocode.core.domain.model.ValidationError
import at.mocode.core.domain.model.ValidationResult
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.masterdata.domain.repository.AltersklasseRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlin.time.Clock
import kotlin.uuid.Uuid
/**
* Use case for creating and updating age class information.
*
* This use case encapsulates the business logic for age class management
* This use case encapsulates the business logic for age class management,
* including validation, duplicate checking, and persistence.
*/
class CreateAltersklasseUseCase(
@@ -137,16 +137,14 @@ class CreateAltersklasseUseCase(
*/
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")
)
}
val existingAltersklasse =
altersklasseRepository.findById(request.altersklasseId) ?: return UpdateAltersklasseResponse(
altersklasse = null,
success = false,
errors = listOf("Age class with ID ${request.altersklasseId} not found")
)
// Validate the request
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
@@ -273,7 +271,7 @@ class CreateAltersklasseUseCase(
* Validates an update age class request.
*/
private fun validateUpdateRequest(request: UpdateAltersklasseRequest): ValidationResult {
// Use the same validation logic as create request
// Use the same validation logic as creation request
val createRequest = CreateAltersklasseRequest(
altersklasseCode = request.altersklasseCode,
bezeichnung = request.bezeichnung,
@@ -1,17 +1,17 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.ValidationError
import at.mocode.core.domain.model.ValidationResult
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.masterdata.domain.repository.BundeslandRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlin.time.Clock
import kotlin.uuid.Uuid
/**
* Use case for creating and updating federal state information.
*
* This use case encapsulates the business logic for federal state management
* This use case encapsulates the business logic for federal state management,
* including validation, duplicate checking, and persistence.
*/
class CreateBundeslandUseCase(
@@ -22,14 +22,14 @@ class CreateBundeslandUseCase(
* 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
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
)
/**
@@ -133,16 +133,13 @@ class CreateBundeslandUseCase(
*/
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")
)
}
val existingBundesland = bundeslandRepository.findById(request.bundeslandId) ?: return UpdateBundeslandResponse(
bundesland = null,
success = false,
errors = listOf("Federal state with ID ${request.bundeslandId} not found")
)
// Validate the request
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
@@ -209,7 +206,7 @@ class CreateBundeslandUseCase(
}
/**
* Validates a create federal state request.
* Validates create federal state request.
*/
private fun validateCreateRequest(request: CreateBundeslandRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
@@ -264,7 +261,7 @@ class CreateBundeslandUseCase(
* Validates an update federal state request.
*/
private fun validateUpdateRequest(request: UpdateBundeslandRequest): ValidationResult {
// Use the same validation logic as create request
// Use the same validation logic as creation request
val createRequest = CreateBundeslandRequest(
landId = request.landId,
oepsCode = request.oepsCode,
@@ -1,17 +1,17 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.ValidationError
import at.mocode.core.domain.model.ValidationResult
import at.mocode.masterdata.domain.model.LandDefinition
import at.mocode.masterdata.domain.repository.LandRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlin.time.Clock
import kotlin.uuid.Uuid
/**
* Use case for creating and updating country information.
*
* This use case encapsulates the business logic for country management
* This use case encapsulates the business logic for country management,
* including validation, duplicate checking, and persistence.
*/
class CreateCountryUseCase(
@@ -139,16 +139,13 @@ class CreateCountryUseCase(
*/
suspend fun updateCountry(request: UpdateCountryRequest): UpdateCountryResponse {
// Check if country exists
val existingCountry = landRepository.findById(request.landId)
if (existingCountry == null) {
return UpdateCountryResponse(
country = null,
success = false,
errors = listOf("Country with ID ${request.landId} not found")
)
}
val existingCountry = landRepository.findById(request.landId) ?: return UpdateCountryResponse(
country = null,
success = false,
errors = listOf("Country with ID ${request.landId} not found")
)
// Validate the request
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
@@ -271,7 +268,7 @@ class CreateCountryUseCase(
* Validates an update country request.
*/
private fun validateUpdateRequest(request: UpdateCountryRequest): ValidationResult {
// Use the same validation logic as create request
// Use the same validation logic as creation request
val createRequest = CreateCountryRequest(
isoAlpha2Code = request.isoAlpha2Code,
isoAlpha3Code = request.isoAlpha3Code,
@@ -2,17 +2,17 @@
package at.mocode.masterdata.application.usecase
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.core.domain.model.ValidationError
import at.mocode.core.domain.model.ValidationResult
import at.mocode.masterdata.domain.model.Platz
import at.mocode.masterdata.domain.repository.PlatzRepository
import at.mocode.core.domain.model.ValidationResult
import at.mocode.core.domain.model.ValidationError
import kotlin.uuid.Uuid
import kotlin.time.Clock
import kotlin.uuid.Uuid
/**
* Use case for creating and updating venue/arena information.
*
* This use case encapsulates the business logic for venue management
* This use case encapsulates the business logic for venue management,
* including validation, duplicate checking, and persistence.
*/
class CreatePlatzUseCase(
@@ -131,16 +131,13 @@ class CreatePlatzUseCase(
*/
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")
)
}
val existingPlatz = platzRepository.findById(request.platzId) ?: return UpdatePlatzResponse(
platz = null,
success = false,
errors = listOf("Venue with ID ${request.platzId} not found")
)
// Validate the request
// Validate the request
val validationResult = validateUpdateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
@@ -251,7 +248,7 @@ class CreatePlatzUseCase(
* Validates an update venue request.
*/
private fun validateUpdateRequest(request: UpdatePlatzRequest): ValidationResult {
// Use the same validation logic as create request
// Use the same validation logic as creation request
val createRequest = CreatePlatzRequest(
turnierId = request.turnierId,
name = request.name,
@@ -384,7 +381,7 @@ class CreatePlatzUseCase(
* This method performs comprehensive checks for tournament venue setup.
*
* @param turnierId The tournament ID
* @param requiredVenueTypes Map of venue type to minimum count required
* @param requiredVenueTypes Map of a venue type to minimum count required
* @return ValidationResult indicating if the tournament has adequate venue setup
*/
suspend fun validateTournamentVenueSetup(
@@ -162,7 +162,7 @@ class GetAltersklasseUseCase(
}
/**
* Validates if a person with given age and gender can participate in an age class.
* Validates if a person with a given age and gender can participate in an age class.
*
* @param altersklasseId The age class ID
* @param age The person's age
@@ -182,7 +182,7 @@ class GetPlatzUseCase(
*
* @param turnierId The tournament ID
* @param activeOnly Whether to include only active venues (default: true)
* @return Map of venue type to list of venues
* @return Map of a venue type to a list of venues
*/
suspend fun getGroupedByTypeForTournament(turnierId: Uuid, activeOnly: Boolean = true): Map<PlatzTypE, List<Platz>> {
val venues = platzRepository.findByTournament(turnierId, activeOnly, true)
@@ -243,7 +243,7 @@ class GetPlatzUseCase(
* @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
* @return Pair of (isValid, reasons) where reasons contain any validation issues
*/
suspend fun validateVenueSuitability(
platzId: Uuid,