refactor: Migrate from monolithic to modular architecture
- Restructure project into domain-specific modules (core, masterdata, members, horses, events, infrastructure) - Create shared client components in common-ui module - Implement CI/CD workflows with GitHub Actions - Consolidate documentation in docs directory - Remove deprecated modules and documentation files - Add cleanup and migration scripts for transition - Update README with new project structure and setup instructions
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("at.mocode.masterdata.api.ApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
// Spring dependencies
|
||||
implementation("org.springframework:spring-web")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-common")
|
||||
|
||||
// Ktor Server
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
implementation(libs.ktor.server.serializationKotlinxJson)
|
||||
implementation(libs.ktor.server.statusPages)
|
||||
implementation(libs.ktor.server.auth)
|
||||
implementation(libs.ktor.server.authJwt)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.ktor.server.tests)
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for country management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* country functionality, following REST conventions and proper error handling.
|
||||
*/
|
||||
class CountryController(
|
||||
private val getCountryUseCase: GetCountryUseCase,
|
||||
private val createCountryUseCase: CreateCountryUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for country API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class CountryDto(
|
||||
val landId: String,
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new country.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateCountryDto(
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing country.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateCountryDto(
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for country endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/countries") {
|
||||
|
||||
// GET /api/masterdata/countries - Get all active countries
|
||||
get {
|
||||
try {
|
||||
// Validate orderBySortierung parameter if provided
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
val orderBySortierung = if (orderBySortierungParam != null) {
|
||||
try {
|
||||
orderBySortierungParam.toBoolean()
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<CountryDto>>("Invalid orderBySortierung parameter. Must be true or false")
|
||||
)
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
val countries = getCountryUseCase.getAllActive(orderBySortierung)
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/{id} - Get country by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
|
||||
|
||||
val country = getCountryUseCase.getById(countryId)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/iso2/{code} - Get country by ISO Alpha-2 code
|
||||
get("/iso2/{code}") {
|
||||
try {
|
||||
val isoCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
|
||||
|
||||
val country = getCountryUseCase.getByIsoAlpha2Code(isoCode)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/iso3/{code} - Get country by ISO Alpha-3 code
|
||||
get("/iso3/{code}") {
|
||||
try {
|
||||
val isoCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
|
||||
|
||||
val country = getCountryUseCase.getByIsoAlpha3Code(isoCode)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/search - Search countries by name
|
||||
get("/search") {
|
||||
try {
|
||||
// Validate query parameters
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
q = call.request.queryParameters["q"]
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<CountryDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
val searchTerm = call.request.queryParameters["q"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>("Search term 'q' is required"))
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
||||
|
||||
val countries = getCountryUseCase.searchByName(searchTerm, limit)
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>(e.message ?: "Invalid search parameters"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to search countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/eu - Get EU member countries
|
||||
get("/eu") {
|
||||
try {
|
||||
val countries = getCountryUseCase.getEuMembers()
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EU countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/ewr - Get EWR member countries
|
||||
get("/ewr") {
|
||||
try {
|
||||
val countries = getCountryUseCase.getEwrMembers()
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EWR countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/masterdata/countries - Create new country
|
||||
post {
|
||||
try {
|
||||
val createDto = call.receive<CreateCountryDto>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateCountryRequest(
|
||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
||||
nameDeutsch = createDto.nameDeutsch,
|
||||
nameEnglisch = createDto.nameEnglisch
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = CreateCountryUseCase.CreateCountryRequest(
|
||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
||||
isoNumerischerCode = createDto.isoNumerischerCode,
|
||||
nameDeutsch = createDto.nameDeutsch,
|
||||
nameEnglisch = createDto.nameEnglisch,
|
||||
wappenUrl = createDto.wappenUrl,
|
||||
istEuMitglied = createDto.istEuMitglied,
|
||||
istEwrMitglied = createDto.istEwrMitglied,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createCountryUseCase.createCountry(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.country!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to create country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/masterdata/countries/{id} - Update existing country
|
||||
put("/{id}") {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
|
||||
|
||||
val updateDto = call.receive<UpdateCountryDto>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateCountryRequest(
|
||||
isoAlpha2Code = updateDto.isoAlpha2Code,
|
||||
isoAlpha3Code = updateDto.isoAlpha3Code,
|
||||
nameDeutsch = updateDto.nameDeutsch,
|
||||
nameEnglisch = updateDto.nameEnglisch
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val request = CreateCountryUseCase.UpdateCountryRequest(
|
||||
landId = countryId,
|
||||
isoAlpha2Code = updateDto.isoAlpha2Code,
|
||||
isoAlpha3Code = updateDto.isoAlpha3Code,
|
||||
isoNumerischerCode = updateDto.isoNumerischerCode,
|
||||
nameDeutsch = updateDto.nameDeutsch,
|
||||
nameEnglisch = updateDto.nameEnglisch,
|
||||
wappenUrl = updateDto.wappenUrl,
|
||||
istEuMitglied = updateDto.istEuMitglied,
|
||||
istEwrMitglied = updateDto.istEwrMitglied,
|
||||
istAktiv = updateDto.istAktiv,
|
||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createCountryUseCase.updateCountry(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.country!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to update country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/masterdata/countries/{id} - Delete country
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid country ID"))
|
||||
|
||||
val result = createCountryUseCase.deleteCountry(countryId)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Country not found: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert LandDefinition domain object to CountryDto.
|
||||
*/
|
||||
private fun LandDefinition.toDto(): CountryDto {
|
||||
return CountryDto(
|
||||
landId = this.landId.toString(),
|
||||
isoAlpha2Code = this.isoAlpha2Code,
|
||||
isoAlpha3Code = this.isoAlpha3Code,
|
||||
isoNumerischerCode = this.isoNumerischerCode,
|
||||
nameDeutsch = this.nameDeutsch,
|
||||
nameEnglisch = this.nameEnglisch,
|
||||
wappenUrl = this.wappenUrl,
|
||||
istEuMitglied = this.istEuMitglied,
|
||||
istEwrMitglied = this.istEwrMitglied,
|
||||
istAktiv = this.istAktiv,
|
||||
sortierReihenfolge = this.sortierReihenfolge,
|
||||
createdAt = this.createdAt.toString(),
|
||||
updatedAt = this.updatedAt.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
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 country information.
|
||||
*
|
||||
* This use case encapsulates the business logic for country management
|
||||
* including validation, duplicate checking, and persistence.
|
||||
*/
|
||||
class CreateCountryUseCase(
|
||||
private val landRepository: LandRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new country.
|
||||
*/
|
||||
data class CreateCountryRequest(
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request data for updating an existing country.
|
||||
*/
|
||||
data class UpdateCountryRequest(
|
||||
val landId: Uuid,
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for country creation.
|
||||
*/
|
||||
data class CreateCountryResponse(
|
||||
val country: LandDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for country update.
|
||||
*/
|
||||
data class UpdateCountryResponse(
|
||||
val country: LandDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for country deletion.
|
||||
*/
|
||||
data class DeleteCountryResponse(
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new country after validation.
|
||||
*
|
||||
* @param request The country creation request
|
||||
* @return CreateCountryResponse with the created country or validation errors
|
||||
*/
|
||||
suspend fun createCountry(request: CreateCountryRequest): CreateCountryResponse {
|
||||
// Validate the request
|
||||
val validationResult = validateCreateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
val duplicateCheck = checkForDuplicates(request.isoAlpha2Code, request.isoAlpha3Code)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val now = Clock.System.now()
|
||||
val country = LandDefinition(
|
||||
isoAlpha2Code = request.isoAlpha2Code.uppercase(),
|
||||
isoAlpha3Code = request.isoAlpha3Code.uppercase(),
|
||||
isoNumerischerCode = request.isoNumerischerCode,
|
||||
nameDeutsch = request.nameDeutsch.trim(),
|
||||
nameEnglisch = request.nameEnglisch?.trim(),
|
||||
wappenUrl = request.wappenUrl?.trim(),
|
||||
istEuMitglied = request.istEuMitglied,
|
||||
istEwrMitglied = request.istEwrMitglied,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedCountry = landRepository.save(country)
|
||||
return CreateCountryResponse(
|
||||
country = savedCountry,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing country after validation.
|
||||
*
|
||||
* @param request The country update request
|
||||
* @return ValidationResult containing the updated country or validation errors
|
||||
*/
|
||||
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")
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateUpdateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates (excluding current country)
|
||||
val duplicateCheck = checkForDuplicatesExcluding(
|
||||
request.isoAlpha2Code,
|
||||
request.isoAlpha3Code,
|
||||
request.landId
|
||||
)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Update the domain object
|
||||
val updatedCountry = existingCountry.copy(
|
||||
isoAlpha2Code = request.isoAlpha2Code.uppercase(),
|
||||
isoAlpha3Code = request.isoAlpha3Code.uppercase(),
|
||||
isoNumerischerCode = request.isoNumerischerCode,
|
||||
nameDeutsch = request.nameDeutsch.trim(),
|
||||
nameEnglisch = request.nameEnglisch?.trim(),
|
||||
wappenUrl = request.wappenUrl?.trim(),
|
||||
istEuMitglied = request.istEuMitglied,
|
||||
istEwrMitglied = request.istEwrMitglied,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedCountry = landRepository.save(updatedCountry)
|
||||
return UpdateCountryResponse(
|
||||
country = savedCountry,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a country by ID.
|
||||
*
|
||||
* @param countryId The unique identifier of the country to delete
|
||||
* @return DeleteCountryResponse indicating success or failure
|
||||
*/
|
||||
suspend fun deleteCountry(countryId: Uuid): DeleteCountryResponse {
|
||||
val deleted = landRepository.delete(countryId)
|
||||
return if (deleted) {
|
||||
DeleteCountryResponse(success = true)
|
||||
} else {
|
||||
DeleteCountryResponse(
|
||||
success = false,
|
||||
errors = listOf("Country with ID $countryId not found or could not be deleted")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a create country request.
|
||||
*/
|
||||
private fun validateCreateRequest(request: CreateCountryRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// ISO Alpha-2 Code validation
|
||||
if (request.isoAlpha2Code.isBlank()) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "ISO Alpha-2 code is required", "REQUIRED"))
|
||||
} else if (request.isoAlpha2Code.length != 2) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "ISO Alpha-2 code must be exactly 2 characters", "INVALID_LENGTH"))
|
||||
} else if (!request.isoAlpha2Code.all { it.isLetter() }) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "ISO Alpha-2 code must contain only letters", "INVALID_FORMAT"))
|
||||
}
|
||||
|
||||
// ISO Alpha-3 Code validation
|
||||
if (request.isoAlpha3Code.isBlank()) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "ISO Alpha-3 code is required", "REQUIRED"))
|
||||
} else if (request.isoAlpha3Code.length != 3) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "ISO Alpha-3 code must be exactly 3 characters", "INVALID_LENGTH"))
|
||||
} else if (!request.isoAlpha3Code.all { it.isLetter() }) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "ISO Alpha-3 code must contain only letters", "INVALID_FORMAT"))
|
||||
}
|
||||
|
||||
// German name validation
|
||||
if (request.nameDeutsch.isBlank()) {
|
||||
errors.add(ValidationError("nameDeutsch", "German name is required", "REQUIRED"))
|
||||
} else if (request.nameDeutsch.length > 100) {
|
||||
errors.add(ValidationError("nameDeutsch", "German name must not exceed 100 characters", "MAX_LENGTH"))
|
||||
}
|
||||
|
||||
// English name validation
|
||||
request.nameEnglisch?.let { name ->
|
||||
if (name.length > 100) {
|
||||
errors.add(ValidationError("nameEnglisch", "English name 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 country request.
|
||||
*/
|
||||
private fun validateUpdateRequest(request: UpdateCountryRequest): ValidationResult {
|
||||
// Use the same validation logic as create request
|
||||
val createRequest = CreateCountryRequest(
|
||||
isoAlpha2Code = request.isoAlpha2Code,
|
||||
isoAlpha3Code = request.isoAlpha3Code,
|
||||
isoNumerischerCode = request.isoNumerischerCode,
|
||||
nameDeutsch = request.nameDeutsch,
|
||||
nameEnglisch = request.nameEnglisch,
|
||||
wappenUrl = request.wappenUrl,
|
||||
istEuMitglied = request.istEuMitglied,
|
||||
istEwrMitglied = request.istEwrMitglied,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge
|
||||
)
|
||||
return validateCreateRequest(createRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate ISO codes.
|
||||
*/
|
||||
private suspend fun checkForDuplicates(isoAlpha2Code: String, isoAlpha3Code: String): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
if (landRepository.existsByIsoAlpha2Code(isoAlpha2Code.uppercase())) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "Country with ISO Alpha-2 code '${isoAlpha2Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
if (landRepository.existsByIsoAlpha3Code(isoAlpha3Code.uppercase())) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "Country with ISO Alpha-3 code '${isoAlpha3Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate ISO codes excluding a specific country ID.
|
||||
*/
|
||||
private suspend fun checkForDuplicatesExcluding(
|
||||
isoAlpha2Code: String,
|
||||
isoAlpha3Code: String,
|
||||
excludeId: Uuid
|
||||
): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Check Alpha-2 code
|
||||
val existingAlpha2 = landRepository.findByIsoAlpha2Code(isoAlpha2Code.uppercase())
|
||||
if (existingAlpha2 != null && existingAlpha2.landId != excludeId) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "Country with ISO Alpha-2 code '${isoAlpha2Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
// Check Alpha-3 code
|
||||
val existingAlpha3 = landRepository.findByIsoAlpha3Code(isoAlpha3Code.uppercase())
|
||||
if (existingAlpha3 != null && existingAlpha3.landId != excludeId) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "Country with ISO Alpha-3 code '${isoAlpha3Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving country information.
|
||||
*
|
||||
* This use case encapsulates the business logic for fetching country data
|
||||
* and provides a clean interface for the application layer.
|
||||
*/
|
||||
class GetCountryUseCase(
|
||||
private val landRepository: LandRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Retrieves a country by its unique ID.
|
||||
*
|
||||
* @param countryId The unique identifier of the country
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(countryId: Uuid): LandDefinition? {
|
||||
return landRepository.findById(countryId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a country by its ISO Alpha-2 code.
|
||||
*
|
||||
* @param isoCode The 2-letter ISO code (e.g., "AT", "DE")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getByIsoAlpha2Code(isoCode: String): LandDefinition? {
|
||||
require(isoCode.length == 2) { "ISO Alpha-2 code must be exactly 2 characters" }
|
||||
return landRepository.findByIsoAlpha2Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a country by its ISO Alpha-3 code.
|
||||
*
|
||||
* @param isoCode The 3-letter ISO code (e.g., "AUT", "DEU")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getByIsoAlpha3Code(isoCode: String): LandDefinition? {
|
||||
require(isoCode.length == 3) { "ISO Alpha-3 code must be exactly 3 characters" }
|
||||
return landRepository.findByIsoAlpha3Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for countries by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against country names
|
||||
* @param limit Maximum number of results to return (default: 50)
|
||||
* @return List of matching countries
|
||||
*/
|
||||
suspend fun searchByName(searchTerm: String, limit: Int = 50): List<LandDefinition> {
|
||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
||||
require(limit > 0) { "Limit must be positive" }
|
||||
return landRepository.findByName(searchTerm.trim(), limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active countries.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of active countries
|
||||
*/
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<LandDefinition> {
|
||||
return landRepository.findAllActive(orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all EU member countries.
|
||||
*
|
||||
* @return List of EU member countries
|
||||
*/
|
||||
suspend fun getEuMembers(): List<LandDefinition> {
|
||||
return landRepository.findEuMembers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all EWR (European Economic Area) member countries.
|
||||
*
|
||||
* @return List of EWR member countries
|
||||
*/
|
||||
suspend fun getEwrMembers(): List<LandDefinition> {
|
||||
return landRepository.findEwrMembers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-2 code exists.
|
||||
*
|
||||
* @param isoCode The ISO Alpha-2 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha2Code(isoCode: String): Boolean {
|
||||
require(isoCode.length == 2) { "ISO Alpha-2 code must be exactly 2 characters" }
|
||||
return landRepository.existsByIsoAlpha2Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-3 code exists.
|
||||
*
|
||||
* @param isoCode The ISO Alpha-3 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha3Code(isoCode: String): Boolean {
|
||||
require(isoCode.length == 3) { "ISO Alpha-3 code must be exactly 3 characters" }
|
||||
return landRepository.existsByIsoAlpha3Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of active countries.
|
||||
*
|
||||
* @return The total count of active countries
|
||||
*/
|
||||
suspend fun countActive(): Long {
|
||||
return landRepository.countActive()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE // Optional, falls Altersklassen stark spartenspezifisch sind
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert eine spezifische Altersklasse für Teilnehmer (Reiter, Fahrer, Voltigierer)
|
||||
* oder ggf. auch für Pferde, basierend auf den Vorgaben der ÖTO oder anderer Regelwerke.
|
||||
*
|
||||
* Beispiele: "Jugend U16", "Junioren U18", "Junge Reiter U21", "Allgemeine Klasse",
|
||||
* "Pony Jugend U14", "Senioren Ü40".
|
||||
* Diese Definitionen dienen zur Überprüfung von Teilnahmeberechtigungen in Bewerben und Abteilungen.
|
||||
*
|
||||
* @property altersklasseId Eindeutiger interner Identifikator für diese Altersklassendefinition (UUID).
|
||||
* @property altersklasseCode Ein eindeutiges Kürzel oder Code für die Altersklasse
|
||||
* (z.B. "JGD_U16", "JUN_U18", "YR_U21", "AK", "PONY_U14"). Dient als fachlicher Schlüssel.
|
||||
* @property bezeichnung Die offizielle oder allgemein verständliche Bezeichnung der Altersklasse.
|
||||
* @property minAlter Das Mindestalter (Jahre, inklusive) für diese Altersklasse. `null`, wenn es keine Untergrenze gibt.
|
||||
* @property maxAlter Das Höchstalter (Jahre, inklusive) für diese Altersklasse. `null`, wenn es keine Obergrenze gibt.
|
||||
* @property stichtagRegelText Eine Beschreibung der Regel für den Stichtag zur Altersberechnung
|
||||
* (z.B. "31.12. des laufenden Kalenderjahres", "Geburtstag im laufenden Jahr").
|
||||
* @property sparteFilter Optionale Angabe, ob diese Altersklassendefinition nur für eine spezifische Sparte gilt.
|
||||
* @property geschlechtFilter Optionaler Filter für das Geschlecht ('M', 'W'), falls die Altersklasse geschlechtsspezifisch ist.
|
||||
* `null` bedeutet für alle Geschlechter gültig.
|
||||
* @property oetoRegelReferenzId Optionale Verknüpfung zu einer spezifischen Regel in der `OETORegelReferenz`-Tabelle,
|
||||
* die diese Altersklasse definiert.
|
||||
* @property istAktiv Gibt an, ob diese Altersklassendefinition aktuell im System verwendet werden kann.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class AltersklasseDefinition(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val altersklasseId: Uuid = uuid4(), // Interner Primärschlüssel
|
||||
|
||||
var altersklasseCode: String, // Fachlicher PK, z.B. "JGD_U16"
|
||||
var bezeichnung: String,
|
||||
var minAlter: Int? = null,
|
||||
var maxAlter: Int? = null,
|
||||
var stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres", // Typischer Default
|
||||
var sparteFilter: SparteE? = null, // Ist diese Definition spartenspezifisch?
|
||||
var geschlechtFilter: Char? = null, // 'M', 'W', oder null für beide
|
||||
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var oetoRegelReferenzId: Uuid? = null, // FK zu OETORegelReferenz.oetoRegelReferenzId
|
||||
|
||||
var istAktiv: Boolean = true,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert ein Bundesland oder eine vergleichbare subnationale Verwaltungseinheit.
|
||||
*
|
||||
* Diese Entität ist primär für die österreichischen Bundesländer mit ihren OEPS-spezifischen
|
||||
* Codes gedacht, kann aber auch für Bundesländer/Regionen anderer Nationen erweitert werden.
|
||||
*
|
||||
* @property bundeslandId Eindeutiger interner Identifikator für dieses Bundesland (UUID).
|
||||
* @property landId Fremdschlüssel zur `LandDefinition`, dem dieses Bundesland angehört.
|
||||
* @property oepsCode Der 2-stellige numerische OEPS-Code für österreichische Bundesländer
|
||||
* (z.B. "01" für Wien, "02" für Niederösterreich). Sollte eindeutig sein für Land "Österreich".
|
||||
* @property iso3166_2_Code Optionaler offizieller ISO 3166-2 Code für das Bundesland
|
||||
* (z.B. "AT-1" für Burgenland, "DE-BY" für Bayern).
|
||||
* @property name Der offizielle Name des Bundeslandes.
|
||||
* @property kuerzel Ein gängiges Kürzel für das Bundesland (z.B. "NÖ", "W", "STMK").
|
||||
* @property wappenUrl Optionaler URL-Pfad zu einer Bilddatei des Bundeslandwappens.
|
||||
* @property istAktiv Gibt an, ob dieses Bundesland aktuell im System ausgewählt/verwendet werden kann.
|
||||
* @property sortierReihenfolge Optionale Zahl zur Steuerung der Sortierreihenfolge in Auswahllisten.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class BundeslandDefinition(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val bundeslandId: Uuid = uuid4(),
|
||||
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var landId: Uuid, // FK zu LandDefinition.landId
|
||||
|
||||
var oepsCode: String?, // z.B. "01", "02", ... für Österreich; eindeutig pro landId = Österreich
|
||||
var iso3166_2_Code: String?, // z.B. "AT-1", "DE-BY"; Eindeutig global oder pro Land?
|
||||
var name: String, // z.B. "Niederösterreich", "Bayern"
|
||||
var kuerzel: String? = null, // z.B. "NÖ", "BY"
|
||||
var wappenUrl: String? = null,
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert ein Land/eine Nation mit seinen offiziellen Codes und Bezeichnungen.
|
||||
*
|
||||
* Diese Entität dient als zentrale Referenz für Länder, die im System für
|
||||
* Nationalitäten von Personen, Vereinen oder für internationale Turniere relevant sind.
|
||||
*
|
||||
* @property landId Eindeutiger interner Identifikator für dieses Land (UUID).
|
||||
* @property isoAlpha2Code Der 2-stellige ISO 3166-1 Alpha-2 Code des Landes (z.B. "AT", "DE"). Sollte eindeutig sein.
|
||||
* @property isoAlpha3Code Der 3-stellige ISO 3166-1 Alpha-3 Code des Landes (z.B. "AUT", "DEU"). Sollte eindeutig sein.
|
||||
* @property isoNumerischerCode Optionaler 3-stelliger numerischer ISO 3166-1 Code des Landes (z.B. "040" für Österreich).
|
||||
* @property nameDeutsch Der offizielle deutsche Name des Landes.
|
||||
* @property nameEnglisch Der offizielle englische Name des Landes.
|
||||
* @property wappenUrl Optionaler URL-Pfad zu einer Bilddatei des Länderwappens oder der Flagge.
|
||||
* @property istEuMitglied Gibt an, ob das Land Mitglied der Europäischen Union ist.
|
||||
* @property istEwrMitglied Gibt an, ob das Land Mitglied des Europäischen Wirtschaftsraums ist.
|
||||
* @property istAktiv Gibt an, ob dieses Land aktuell im System ausgewählt/verwendet werden kann.
|
||||
* @property sortierReihenfolge Optionale Zahl zur Steuerung der Sortierreihenfolge in Auswahllisten.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class LandDefinition(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val landId: Uuid = uuid4(),
|
||||
|
||||
var isoAlpha2Code: String, // z.B. "AT" → Fachlicher PK oder Unique Constraint
|
||||
var isoAlpha3Code: String, // z.B. "AUT" -> Unique Constraint
|
||||
var isoNumerischerCode: String? = null, // z.B. "040"
|
||||
var nameDeutsch: String, // z.B. "Österreich"
|
||||
var nameEnglisch: String? = null, // z.B. "Austria"
|
||||
var wappenUrl: String? = null,
|
||||
var istEuMitglied: Boolean? = null,
|
||||
var istEwrMitglied: Boolean? = null, // Europäischer Wirtschaftsraum
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Platz(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val id: Uuid = uuid4(),
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var turnierId: Uuid,
|
||||
var name: String,
|
||||
var dimension: String?,
|
||||
var boden: String?,
|
||||
var typ: PlatzTypE
|
||||
)
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package at.mocode.masterdata.domain.repository
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository interface for LandDefinition (Country) domain operations.
|
||||
*
|
||||
* This interface defines the contract for country data access operations
|
||||
* without depending on specific implementation details (database, etc.).
|
||||
* Following the hexagonal architecture pattern, this interface belongs
|
||||
* to the domain layer and will be implemented in the infrastructure layer.
|
||||
*/
|
||||
interface LandRepository {
|
||||
|
||||
/**
|
||||
* Finds a country by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the country
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): LandDefinition?
|
||||
|
||||
/**
|
||||
* Finds a country by its ISO Alpha-2 code.
|
||||
*
|
||||
* @param isoAlpha2Code The 2-letter ISO code (e.g., "AT", "DE")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition?
|
||||
|
||||
/**
|
||||
* Finds a country by its ISO Alpha-3 code.
|
||||
*
|
||||
* @param isoAlpha3Code The 3-letter ISO code (e.g., "AUT", "DEU")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition?
|
||||
|
||||
/**
|
||||
* Finds countries by name (partial match on German or English name).
|
||||
*
|
||||
* @param searchTerm The search term to match against country names
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching countries
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all active countries.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field
|
||||
* @return List of active countries
|
||||
*/
|
||||
suspend fun findAllActive(orderBySortierung: Boolean = true): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all EU member countries.
|
||||
*
|
||||
* @return List of EU member countries
|
||||
*/
|
||||
suspend fun findEuMembers(): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all EWR (European Economic Area) member countries.
|
||||
*
|
||||
* @return List of EWR member countries
|
||||
*/
|
||||
suspend fun findEwrMembers(): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Saves a country (create or update).
|
||||
*
|
||||
* @param land The country to save
|
||||
* @return The saved country with updated timestamps
|
||||
*/
|
||||
suspend fun save(land: LandDefinition): LandDefinition
|
||||
|
||||
/**
|
||||
* Deletes a country by ID.
|
||||
*
|
||||
* @param id The unique identifier of the country to delete
|
||||
* @return true if the country was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-2 code exists.
|
||||
*
|
||||
* @param isoAlpha2Code The ISO Alpha-2 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-3 code exists.
|
||||
*
|
||||
* @param isoAlpha3Code The ISO Alpha-3 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean
|
||||
|
||||
/**
|
||||
* Counts the total number of active countries.
|
||||
*
|
||||
* @return The total count of active countries
|
||||
*/
|
||||
suspend fun countActive(): Long
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
kotlin("plugin.jpa") version "2.1.20"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.cache.cacheApi)
|
||||
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.postgresql:postgresql")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import at.mocode.masterdata.infrastructure.persistence.LandTable
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des LandRepository für die Datenbankzugriffe.
|
||||
*/
|
||||
class LandRepositoryImpl : LandRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToLandDefinition(row: ResultRow): LandDefinition {
|
||||
return LandDefinition(
|
||||
landId = row[LandTable.id],
|
||||
isoAlpha2Code = row[LandTable.isoAlpha2Code],
|
||||
isoAlpha3Code = row[LandTable.isoAlpha3Code],
|
||||
nameDeutsch = row[LandTable.nameDe],
|
||||
nameEnglisch = row[LandTable.nameEn],
|
||||
istEuMitglied = row[LandTable.istEuMitglied],
|
||||
istEwrMitglied = row[LandTable.istEwrMitglied],
|
||||
sortierReihenfolge = row[LandTable.sortierReihenfolge],
|
||||
istAktiv = row[LandTable.istAktiv],
|
||||
createdAt = row[LandTable.erstelltAm].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[LandTable.geaendertAm].toInstant(TimeZone.UTC)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.id eq id }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
LandTable.selectAll().where { (LandTable.nameDe like pattern) or (LandTable.nameEn like pattern) }
|
||||
.limit(limit)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = LandTable.selectAll().where { LandTable.istAktiv eq true }
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDe to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(LandTable.nameDe to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findEuMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { (LandTable.istEuMitglied eq true) and (LandTable.istAktiv eq true) }
|
||||
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDe to SortOrder.ASC)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findEwrMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { (LandTable.istEwrMitglied eq true) and (LandTable.istAktiv eq true) }
|
||||
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDe to SortOrder.ASC)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(land: LandDefinition): LandDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingLand = LandTable.selectAll().where { LandTable.id eq land.landId }.singleOrNull()
|
||||
|
||||
if (existingLand == null) {
|
||||
// Insert a new country
|
||||
LandTable.insert { stmt ->
|
||||
stmt[id] = land.landId
|
||||
stmt[isoAlpha2Code] = land.isoAlpha2Code
|
||||
stmt[isoAlpha3Code] = land.isoAlpha3Code
|
||||
stmt[nameDe] = land.nameDeutsch
|
||||
stmt[nameEn] = land.nameEnglisch ?: ""
|
||||
stmt[istEuMitglied] = land.istEuMitglied ?: false
|
||||
stmt[istEwrMitglied] = land.istEwrMitglied ?: false
|
||||
stmt[sortierReihenfolge] = land.sortierReihenfolge ?: 999
|
||||
stmt[istAktiv] = land.istAktiv
|
||||
stmt[erstelltAm] = land.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[geaendertAm] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing country
|
||||
LandTable.update({ LandTable.id eq land.landId }) { stmt ->
|
||||
stmt[isoAlpha2Code] = land.isoAlpha2Code
|
||||
stmt[isoAlpha3Code] = land.isoAlpha3Code
|
||||
stmt[nameDe] = land.nameDeutsch
|
||||
stmt[nameEn] = land.nameEnglisch ?: ""
|
||||
stmt[istEuMitglied] = land.istEuMitglied ?: false
|
||||
stmt[istEwrMitglied] = land.istEwrMitglied ?: false
|
||||
stmt[sortierReihenfolge] = land.sortierReihenfolge ?: 999
|
||||
stmt[istAktiv] = land.istAktiv
|
||||
stmt[geaendertAm] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
land.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.deleteWhere { LandTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.istAktiv eq true }.count()
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für die Land-Entität (Länderstammdaten).
|
||||
*/
|
||||
object LandTable : Table("land") {
|
||||
val id = uuid("id").autoGenerate()
|
||||
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
|
||||
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
|
||||
val nameDe = varchar("name_de", 100)
|
||||
val nameEn = varchar("name_en", 100)
|
||||
val istEuMitglied = bool("ist_eu_mitglied").default(false)
|
||||
val istEwrMitglied = bool("ist_ewr_mitglied").default(false)
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").default(999)
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val erstelltAm = datetime("erstellt_am").defaultExpression(CurrentDateTime)
|
||||
val geaendertAm = datetime("geaendert_am").defaultExpression(CurrentDateTime)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
id("org.springframework.boot")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.masterdata.service.MasterdataServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.masterdata.masterdataInfrastructure)
|
||||
implementation(projects.masterdata.masterdataApi)
|
||||
|
||||
implementation(projects.infrastructure.auth.authClient)
|
||||
implementation(projects.infrastructure.cache.redisCache)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui")
|
||||
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package at.mocode.masterdata.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
/**
|
||||
* Main application class for the Masterdata Service.
|
||||
*
|
||||
* This service provides APIs for managing master data such as countries, regions, and other reference data.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
class MasterdataServiceApplication
|
||||
|
||||
/**
|
||||
* Main entry point for the Masterdata Service application.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MasterdataServiceApplication>(*args)
|
||||
}
|
||||
Reference in New Issue
Block a user