Umbau zu SCS
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(project(":shared-kernel"))
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.uuid)
|
||||
implementation(libs.bignum)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
implementation(libs.ktor.server.serializationKotlinxJson)
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.postgresql.driver)
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
// Kotlin React dependencies with explicit versions
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:${libs.versions.kotlinWrappers.get()}")
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:${libs.versions.kotlinWrappers.get()}")
|
||||
|
||||
// NPM dependencies
|
||||
implementation(npm("react", "18.2.0"))
|
||||
implementation(npm("react-dom", "18.2.0"))
|
||||
implementation(npm("react-to-web-component", "2.0.2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import at.mocode.validation.ValidationResult
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new country after validation.
|
||||
*
|
||||
* @param request The country creation request
|
||||
* @return ValidationResult containing the created country or validation errors
|
||||
*/
|
||||
suspend fun createCountry(request: CreateCountryRequest): ValidationResult<LandDefinition> {
|
||||
// Validate the request
|
||||
val validationResult = validateCreateRequest(request)
|
||||
if (!validationResult.isValid) {
|
||||
return ValidationResult.failure(validationResult.errors)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
val duplicateCheck = checkForDuplicates(request.isoAlpha2Code, request.isoAlpha3Code)
|
||||
if (!duplicateCheck.isValid) {
|
||||
return ValidationResult.failure(duplicateCheck.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 ValidationResult.success(savedCountry)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): ValidationResult<LandDefinition> {
|
||||
// Check if country exists
|
||||
val existingCountry = landRepository.findById(request.landId)
|
||||
?: return ValidationResult.failure(listOf("Country with ID ${request.landId} not found"))
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateUpdateRequest(request)
|
||||
if (!validationResult.isValid) {
|
||||
return ValidationResult.failure(validationResult.errors)
|
||||
}
|
||||
|
||||
// Check for duplicates (excluding current country)
|
||||
val duplicateCheck = checkForDuplicatesExcluding(
|
||||
request.isoAlpha2Code,
|
||||
request.isoAlpha3Code,
|
||||
request.landId
|
||||
)
|
||||
if (!duplicateCheck.isValid) {
|
||||
return ValidationResult.failure(duplicateCheck.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 ValidationResult.success(savedCountry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a country by ID.
|
||||
*
|
||||
* @param countryId The unique identifier of the country to delete
|
||||
* @return ValidationResult indicating success or failure
|
||||
*/
|
||||
suspend fun deleteCountry(countryId: Uuid): ValidationResult<Unit> {
|
||||
val deleted = landRepository.delete(countryId)
|
||||
return if (deleted) {
|
||||
ValidationResult.success(Unit)
|
||||
} else {
|
||||
ValidationResult.failure(listOf("Country with ID $countryId not found or could not be deleted"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a create country request.
|
||||
*/
|
||||
private fun validateCreateRequest(request: CreateCountryRequest): ValidationResult<Unit> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
// ISO Alpha-2 Code validation
|
||||
if (request.isoAlpha2Code.isBlank()) {
|
||||
errors.add("ISO Alpha-2 code is required")
|
||||
} else if (request.isoAlpha2Code.length != 2) {
|
||||
errors.add("ISO Alpha-2 code must be exactly 2 characters")
|
||||
} else if (!request.isoAlpha2Code.all { it.isLetter() }) {
|
||||
errors.add("ISO Alpha-2 code must contain only letters")
|
||||
}
|
||||
|
||||
// ISO Alpha-3 Code validation
|
||||
if (request.isoAlpha3Code.isBlank()) {
|
||||
errors.add("ISO Alpha-3 code is required")
|
||||
} else if (request.isoAlpha3Code.length != 3) {
|
||||
errors.add("ISO Alpha-3 code must be exactly 3 characters")
|
||||
} else if (!request.isoAlpha3Code.all { it.isLetter() }) {
|
||||
errors.add("ISO Alpha-3 code must contain only letters")
|
||||
}
|
||||
|
||||
// German name validation
|
||||
if (request.nameDeutsch.isBlank()) {
|
||||
errors.add("German name is required")
|
||||
} else if (request.nameDeutsch.length > 100) {
|
||||
errors.add("German name must not exceed 100 characters")
|
||||
}
|
||||
|
||||
// English name validation
|
||||
request.nameEnglisch?.let { name ->
|
||||
if (name.length > 100) {
|
||||
errors.add("English name must not exceed 100 characters")
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting order validation
|
||||
request.sortierReihenfolge?.let { order ->
|
||||
if (order < 0) {
|
||||
errors.add("Sorting order must be non-negative")
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.success(Unit)
|
||||
} else {
|
||||
ValidationResult.failure(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an update country request.
|
||||
*/
|
||||
private fun validateUpdateRequest(request: UpdateCountryRequest): ValidationResult<Unit> {
|
||||
// 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<Unit> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
if (landRepository.existsByIsoAlpha2Code(isoAlpha2Code.uppercase())) {
|
||||
errors.add("Country with ISO Alpha-2 code '${isoAlpha2Code.uppercase()}' already exists")
|
||||
}
|
||||
|
||||
if (landRepository.existsByIsoAlpha3Code(isoAlpha3Code.uppercase())) {
|
||||
errors.add("Country with ISO Alpha-3 code '${isoAlpha3Code.uppercase()}' already exists")
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.success(Unit)
|
||||
} else {
|
||||
ValidationResult.failure(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate ISO codes excluding a specific country ID.
|
||||
*/
|
||||
private suspend fun checkForDuplicatesExcluding(
|
||||
isoAlpha2Code: String,
|
||||
isoAlpha3Code: String,
|
||||
excludeId: Uuid
|
||||
): ValidationResult<Unit> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
// Check Alpha-2 code
|
||||
val existingAlpha2 = landRepository.findByIsoAlpha2Code(isoAlpha2Code.uppercase())
|
||||
if (existingAlpha2 != null && existingAlpha2.landId != excludeId) {
|
||||
errors.add("Country with ISO Alpha-2 code '${isoAlpha2Code.uppercase()}' already exists")
|
||||
}
|
||||
|
||||
// Check Alpha-3 code
|
||||
val existingAlpha3 = landRepository.findByIsoAlpha3Code(isoAlpha3Code.uppercase())
|
||||
if (existingAlpha3 != null && existingAlpha3.landId != excludeId) {
|
||||
errors.add("Country with ISO Alpha-3 code '${isoAlpha3Code.uppercase()}' already exists")
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.success(Unit)
|
||||
} else {
|
||||
ValidationResult.failure(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()
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.enums.SparteE // Optional, falls Altersklassen stark spartenspezifisch sind
|
||||
import at.mocode.serializers.KotlinInstantSerializer
|
||||
import at.mocode.serializers.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.serializers.KotlinInstantSerializer
|
||||
import at.mocode.serializers.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.serializers.KotlinInstantSerializer
|
||||
import at.mocode.serializers.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.enums.PlatzTypE
|
||||
import at.mocode.serializers.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
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
package at.mocode.masterdata.infrastructure.api
|
||||
|
||||
import at.mocode.dto.base.BaseDto
|
||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
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 {
|
||||
val orderBySortierung = call.request.queryParameters["orderBySortierung"]?.toBoolean() ?: true
|
||||
val countries = getCountryUseCase.getAllActive(orderBySortierung)
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.error<CountryDto>("Invalid country ID"))
|
||||
|
||||
val country = getCountryUseCase.getById(countryId)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.error<CountryDto>("ISO code is required"))
|
||||
|
||||
val country = getCountryUseCase.getByIsoAlpha2Code(isoCode)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.error<CountryDto>("ISO code is required"))
|
||||
|
||||
val country = getCountryUseCase.getByIsoAlpha3Code(isoCode)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/search - Search countries by name
|
||||
get("/search") {
|
||||
try {
|
||||
val searchTerm = call.request.queryParameters["q"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, BaseDto.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, BaseDto.success(countryDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<List<CountryDto>>(e.message ?: "Invalid search parameters"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.error<List<CountryDto>>("Failed to retrieve EWR countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/masterdata/countries - Create new country
|
||||
post {
|
||||
try {
|
||||
val createDto = call.receive<CreateCountryDto>()
|
||||
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.isValid) {
|
||||
call.respond(HttpStatusCode.Created, BaseDto.success(result.data!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<CountryDto>("Validation failed", result.errors))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.error<CountryDto>("Invalid country ID"))
|
||||
|
||||
val updateDto = call.receive<UpdateCountryDto>()
|
||||
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.isValid) {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(result.data!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, BaseDto.error<CountryDto>("Validation failed", result.errors))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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, BaseDto.error<Unit>("Invalid country ID"))
|
||||
|
||||
val result = createCountryUseCase.deleteCountry(countryId)
|
||||
if (result.isValid) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, BaseDto.error<Unit>("Country not found", result.errors))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, BaseDto.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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package at.mocode.masterdata.infrastructure.repository
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
|
||||
|
||||
/**
|
||||
* PostgreSQL implementation of LandRepository using Exposed ORM.
|
||||
*
|
||||
* This implementation provides data access operations for country data,
|
||||
* mapping between the domain model (LandDefinition) and the database table (LandTable).
|
||||
*/
|
||||
class LandRepositoryImpl : LandRepository {
|
||||
|
||||
override suspend fun findById(id: Uuid): LandDefinition? {
|
||||
return LandTable.select { LandTable.id eq id }
|
||||
.singleOrNull()
|
||||
?.toLandDefinition()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? {
|
||||
return LandTable.select { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.singleOrNull()
|
||||
?.toLandDefinition()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? {
|
||||
return LandTable.select { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.singleOrNull()
|
||||
?.toLandDefinition()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> {
|
||||
val searchPattern = "%$searchTerm%"
|
||||
return LandTable.select {
|
||||
(LandTable.nameGerman like searchPattern) or
|
||||
(LandTable.nameEnglish like searchPattern) or
|
||||
(LandTable.nameLocal like searchPattern)
|
||||
}
|
||||
.orderBy(LandTable.sortierReihenfolge)
|
||||
.limit(limit)
|
||||
.map { it.toLandDefinition() }
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<LandDefinition> {
|
||||
val query = LandTable.select { LandTable.isActive eq true }
|
||||
|
||||
return if (orderBySortierung) {
|
||||
query.orderBy(LandTable.sortierReihenfolge, LandTable.nameGerman)
|
||||
} else {
|
||||
query.orderBy(LandTable.nameGerman)
|
||||
}.map { it.toLandDefinition() }
|
||||
}
|
||||
|
||||
override suspend fun findEuMembers(): List<LandDefinition> {
|
||||
return LandTable.select {
|
||||
(LandTable.isActive eq true) and (LandTable.isEuMember eq true)
|
||||
}
|
||||
.orderBy(LandTable.sortierReihenfolge, LandTable.nameGerman)
|
||||
.map { it.toLandDefinition() }
|
||||
}
|
||||
|
||||
override suspend fun findEwrMembers(): List<LandDefinition> {
|
||||
return LandTable.select {
|
||||
(LandTable.isActive eq true) and (LandTable.isEwrMember eq true)
|
||||
}
|
||||
.orderBy(LandTable.sortierReihenfolge, LandTable.nameGerman)
|
||||
.map { it.toLandDefinition() }
|
||||
}
|
||||
|
||||
override suspend fun save(land: LandDefinition): LandDefinition {
|
||||
val now = Clock.System.now()
|
||||
|
||||
// Check if record exists
|
||||
val existingRecord = LandTable.select { LandTable.id eq land.landId }.singleOrNull()
|
||||
|
||||
return if (existingRecord != null) {
|
||||
// Update existing record
|
||||
LandTable.update({ LandTable.id eq land.landId }) {
|
||||
it[isoAlpha2Code] = land.isoAlpha2Code
|
||||
it[isoAlpha3Code] = land.isoAlpha3Code
|
||||
it[isoNumericCode] = land.isoNumerischerCode
|
||||
it[nameGerman] = land.nameDeutsch
|
||||
it[nameEnglish] = land.nameEnglisch
|
||||
it[nameLocal] = land.nameEnglisch // Using English as local fallback
|
||||
it[isActive] = land.istAktiv
|
||||
it[isEuMember] = land.istEuMitglied ?: false
|
||||
it[isEwrMember] = land.istEwrMitglied ?: false
|
||||
it[sortierReihenfolge] = land.sortierReihenfolge ?: 999
|
||||
it[flagIcon] = land.wappenUrl
|
||||
it[updatedAt] = now
|
||||
it[notes] = null // Could be extended later
|
||||
}
|
||||
land.copy(updatedAt = now)
|
||||
} else {
|
||||
// Insert new record
|
||||
LandTable.insert {
|
||||
it[id] = land.landId
|
||||
it[isoAlpha2Code] = land.isoAlpha2Code
|
||||
it[isoAlpha3Code] = land.isoAlpha3Code
|
||||
it[isoNumericCode] = land.isoNumerischerCode
|
||||
it[nameGerman] = land.nameDeutsch
|
||||
it[nameEnglish] = land.nameEnglisch
|
||||
it[nameLocal] = land.nameEnglisch // Using English as local fallback
|
||||
it[isActive] = land.istAktiv
|
||||
it[isEuMember] = land.istEuMitglied ?: false
|
||||
it[isEwrMember] = land.istEwrMitglied ?: false
|
||||
it[sortierReihenfolge] = land.sortierReihenfolge ?: 999
|
||||
it[flagIcon] = land.wappenUrl
|
||||
it[createdAt] = land.createdAt
|
||||
it[updatedAt] = now
|
||||
it[notes] = null
|
||||
}
|
||||
land.copy(updatedAt = now)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean {
|
||||
val deletedRows = LandTable.deleteWhere { LandTable.id eq id }
|
||||
return deletedRows > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean {
|
||||
return LandTable.select { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean {
|
||||
return LandTable.select { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long {
|
||||
return LandTable.select { LandTable.isActive eq true }.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert a database ResultRow to a LandDefinition domain object.
|
||||
*/
|
||||
private fun ResultRow.toLandDefinition(): LandDefinition {
|
||||
return LandDefinition(
|
||||
landId = this[LandTable.id].value,
|
||||
isoAlpha2Code = this[LandTable.isoAlpha2Code],
|
||||
isoAlpha3Code = this[LandTable.isoAlpha3Code],
|
||||
isoNumerischerCode = this[LandTable.isoNumericCode],
|
||||
nameDeutsch = this[LandTable.nameGerman],
|
||||
nameEnglisch = this[LandTable.nameEnglish],
|
||||
wappenUrl = this[LandTable.flagIcon],
|
||||
istEuMitglied = this[LandTable.isEuMember],
|
||||
istEwrMitglied = this[LandTable.isEwrMember],
|
||||
istAktiv = this[LandTable.isActive],
|
||||
sortierReihenfolge = this[LandTable.sortierReihenfolge],
|
||||
createdAt = this[LandTable.createdAt],
|
||||
updatedAt = this[LandTable.updatedAt]
|
||||
)
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package at.mocode.masterdata.infrastructure.repository
|
||||
|
||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Database table definition for LandDefinition (Country) entities.
|
||||
*
|
||||
* This table stores country reference data including ISO codes,
|
||||
* names in multiple languages, and EU/EWR membership information.
|
||||
*/
|
||||
object LandTable : UUIDTable("land_definition") {
|
||||
|
||||
// ISO Codes
|
||||
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
|
||||
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
|
||||
val isoNumericCode = varchar("iso_numeric_code", 3).nullable()
|
||||
|
||||
// Names
|
||||
val nameGerman = varchar("name_german", 100)
|
||||
val nameEnglish = varchar("name_english", 100)
|
||||
val nameLocal = varchar("name_local", 100).nullable()
|
||||
|
||||
// Status and Membership
|
||||
val isActive = bool("is_active").default(true)
|
||||
val isEuMember = bool("is_eu_member").default(false)
|
||||
val isEwrMember = bool("is_ewr_member").default(false)
|
||||
|
||||
// Sorting and Display
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").default(999)
|
||||
val flagIcon = varchar("flag_icon", 10).nullable()
|
||||
|
||||
// Audit fields
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
val createdBy = varchar("created_by", 50).nullable()
|
||||
val updatedBy = varchar("updated_by", 50).nullable()
|
||||
|
||||
// Additional metadata
|
||||
val notes = text("notes").nullable()
|
||||
}
|
||||
Reference in New Issue
Block a user