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:
stefan
2025-07-22 18:44:18 +02:00
parent 8229e8e571
commit a256622f37
314 changed files with 5930 additions and 19817 deletions
@@ -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)
}
@@ -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)
}
@@ -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)
}
}
}
@@ -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)
}
@@ -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()
)
@@ -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()
)
@@ -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()
)
@@ -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
)
@@ -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)
}
@@ -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()
}
}
@@ -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)
}
@@ -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)
}