Umbau zu SCS

This commit is contained in:
stefan
2025-07-17 15:17:31 +02:00
parent 67c52f7381
commit 029b0c86bc
255 changed files with 6458 additions and 26663 deletions
@@ -0,0 +1,203 @@
package at.mocode.members.application.usecase
import at.mocode.dto.base.ApiResponse
import at.mocode.dto.base.ErrorDto
import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.members.domain.repository.VereinRepository
import at.mocode.members.domain.service.MasterDataService
import kotlinx.datetime.Clock
/**
* Use case for creating a new person in the member management context.
*
* This use case handles the business logic for person creation including:
* - Validation of input data
* - Checking for duplicate OEPS Satznummer
* - Validation of referenced entities (club, country)
* - Person creation and persistence
*/
class CreatePersonUseCase(
private val personRepository: PersonRepository,
private val vereinRepository: VereinRepository,
private val masterDataService: MasterDataService
) {
/**
* Request data for creating a person.
*/
data class CreatePersonRequest(
val oepsSatzNr: String?,
val nachname: String,
val vorname: String,
val titel: String? = null,
val geburtsdatum: kotlinx.datetime.LocalDate? = null,
val geschlechtE: at.mocode.enums.GeschlechtE? = null,
val nationalitaetLandId: com.benasher44.uuid.Uuid? = null,
val feiId: String? = null,
val telefon: String? = null,
val email: String? = null,
val strasse: String? = null,
val plz: String? = null,
val ort: String? = null,
val adresszusatzZusatzinfo: String? = null,
val stammVereinId: com.benasher44.uuid.Uuid? = null,
val mitgliedsNummerBeiStammVerein: String? = null,
val istGesperrt: Boolean = false,
val sperrGrund: String? = null,
val altersklasseOepsCodeRaw: String? = null,
val istJungerReiterOepsFlag: Boolean = false,
val kaderStatusOepsRaw: String? = null,
val datenQuelle: at.mocode.enums.DatenQuelleE = at.mocode.enums.DatenQuelleE.MANUELL,
val notizenIntern: String? = null
)
/**
* Response data for person creation.
*/
data class CreatePersonResponse(
val person: DomPerson
)
/**
* Executes the create person use case.
*
* @param request The person creation request
* @return ApiResponse containing the created person or error information
*/
suspend fun execute(request: CreatePersonRequest): ApiResponse<CreatePersonResponse> {
try {
// Validate required fields
val validationErrors = validateRequest(request)
if (validationErrors.isNotEmpty()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Invalid input data",
details = validationErrors
)
)
}
// Check for duplicate OEPS Satznummer
if (request.oepsSatzNr != null) {
if (personRepository.existsByOepsSatzNr(request.oepsSatzNr)) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "DUPLICATE_OEPS_SATZNR",
message = "A person with this OEPS Satznummer already exists"
)
)
}
}
// Validate referenced entities
val entityValidationErrors = validateReferencedEntities(request)
if (entityValidationErrors.isNotEmpty()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_REFERENCES",
message = "Referenced entities not found",
details = entityValidationErrors
)
)
}
// Create the person
val person = DomPerson(
oepsSatzNr = request.oepsSatzNr,
nachname = request.nachname,
vorname = request.vorname,
titel = request.titel,
geburtsdatum = request.geburtsdatum,
geschlechtE = request.geschlechtE,
nationalitaetLandId = request.nationalitaetLandId,
feiId = request.feiId,
telefon = request.telefon,
email = request.email,
strasse = request.strasse,
plz = request.plz,
ort = request.ort,
adresszusatzZusatzinfo = request.adresszusatzZusatzinfo,
stammVereinId = request.stammVereinId,
mitgliedsNummerBeiStammVerein = request.mitgliedsNummerBeiStammVerein,
istGesperrt = request.istGesperrt,
sperrGrund = request.sperrGrund,
altersklasseOepsCodeRaw = request.altersklasseOepsCodeRaw,
istJungerReiterOepsFlag = request.istJungerReiterOepsFlag,
kaderStatusOepsRaw = request.kaderStatusOepsRaw,
datenQuelle = request.datenQuelle,
notizenIntern = request.notizenIntern,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
// Save the person
val savedPerson = personRepository.save(person)
return ApiResponse(
success = true,
data = CreatePersonResponse(savedPerson)
)
} catch (e: Exception) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while creating the person: ${e.message}"
)
)
}
}
private fun validateRequest(request: CreatePersonRequest): Map<String, String> {
val errors = mutableMapOf<String, String>()
if (request.nachname.isBlank()) {
errors["nachname"] = "Last name is required"
}
if (request.vorname.isBlank()) {
errors["vorname"] = "First name is required"
}
if (request.oepsSatzNr != null && request.oepsSatzNr.length != 6) {
errors["oepsSatzNr"] = "OEPS Satznummer must be exactly 6 digits"
}
if (request.email != null && !isValidEmail(request.email)) {
errors["email"] = "Invalid email format"
}
return errors
}
private suspend fun validateReferencedEntities(request: CreatePersonRequest): Map<String, String> {
val errors = mutableMapOf<String, String>()
// Validate club reference
if (request.stammVereinId != null) {
val verein = vereinRepository.findById(request.stammVereinId)
if (verein == null) {
errors["stammVereinId"] = "Referenced club not found"
}
}
// Validate country reference
if (request.nationalitaetLandId != null) {
if (!masterDataService.countryExists(request.nationalitaetLandId)) {
errors["nationalitaetLandId"] = "Referenced country not found"
}
}
return errors
}
private fun isValidEmail(email: String): Boolean {
return email.contains("@") && email.contains(".")
}
}
@@ -0,0 +1,182 @@
package at.mocode.members.application.usecase
import at.mocode.dto.base.ApiResponse
import at.mocode.dto.base.ErrorDto
import at.mocode.members.domain.model.DomVerein
import at.mocode.members.domain.repository.VereinRepository
import at.mocode.members.domain.service.MasterDataService
import kotlinx.datetime.Clock
/**
* Use case for creating a new club/association in the member management context.
*
* This use case handles the business logic for club creation including:
* - Validation of input data
* - Checking for duplicate OEPS Vereinsnummer
* - Validation of referenced entities (country, state)
* - Club creation and persistence
*/
class CreateVereinUseCase(
private val vereinRepository: VereinRepository,
private val masterDataService: MasterDataService
) {
/**
* Request data for creating a club.
*/
data class CreateVereinRequest(
val oepsVereinsNr: String?,
val name: String,
val kuerzel: String? = null,
val adresseStrasse: String? = null,
val plz: String? = null,
val ort: String? = null,
val bundeslandId: com.benasher44.uuid.Uuid? = null,
val landId: com.benasher44.uuid.Uuid,
val emailAllgemein: String? = null,
val telefonAllgemein: String? = null,
val webseiteUrl: String? = null,
val datenQuelle: at.mocode.enums.DatenQuelleE = at.mocode.enums.DatenQuelleE.MANUELL,
val notizenIntern: String? = null
)
/**
* Response data for club creation.
*/
data class CreateVereinResponse(
val verein: DomVerein
)
/**
* Executes the create club use case.
*
* @param request The club creation request
* @return ApiResponse containing the created club or error information
*/
suspend fun execute(request: CreateVereinRequest): ApiResponse<CreateVereinResponse> {
try {
// Validate required fields
val validationErrors = validateRequest(request)
if (validationErrors.isNotEmpty()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Invalid input data",
details = validationErrors
)
)
}
// Check for duplicate OEPS Vereinsnummer
if (request.oepsVereinsNr != null) {
if (vereinRepository.existsByOepsVereinsNr(request.oepsVereinsNr)) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "DUPLICATE_OEPS_VEREINSNR",
message = "A club with this OEPS Vereinsnummer already exists"
)
)
}
}
// Validate referenced entities
val entityValidationErrors = validateReferencedEntities(request)
if (entityValidationErrors.isNotEmpty()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_REFERENCES",
message = "Referenced entities not found",
details = entityValidationErrors
)
)
}
// Create the club
val verein = DomVerein(
oepsVereinsNr = request.oepsVereinsNr,
name = request.name,
kuerzel = request.kuerzel,
adresseStrasse = request.adresseStrasse,
plz = request.plz,
ort = request.ort,
bundeslandId = request.bundeslandId,
landId = request.landId,
emailAllgemein = request.emailAllgemein,
telefonAllgemein = request.telefonAllgemein,
webseiteUrl = request.webseiteUrl,
datenQuelle = request.datenQuelle,
notizenIntern = request.notizenIntern,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
// Save the club
val savedVerein = vereinRepository.save(verein)
return ApiResponse(
success = true,
data = CreateVereinResponse(savedVerein)
)
} catch (e: Exception) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while creating the club: ${e.message}"
)
)
}
}
private fun validateRequest(request: CreateVereinRequest): Map<String, String> {
val errors = mutableMapOf<String, String>()
if (request.name.isBlank()) {
errors["name"] = "Club name is required"
}
if (request.oepsVereinsNr != null && request.oepsVereinsNr.length != 4) {
errors["oepsVereinsNr"] = "OEPS Vereinsnummer must be exactly 4 digits"
}
if (request.emailAllgemein != null && !isValidEmail(request.emailAllgemein)) {
errors["emailAllgemein"] = "Invalid email format"
}
if (request.webseiteUrl != null && !isValidUrl(request.webseiteUrl)) {
errors["webseiteUrl"] = "Invalid URL format"
}
return errors
}
private suspend fun validateReferencedEntities(request: CreateVereinRequest): Map<String, String> {
val errors = mutableMapOf<String, String>()
// Validate country reference (required)
if (!masterDataService.countryExists(request.landId)) {
errors["landId"] = "Referenced country not found"
}
// Validate state reference (optional)
if (request.bundeslandId != null) {
if (!masterDataService.stateExists(request.bundeslandId)) {
errors["bundeslandId"] = "Referenced state not found"
}
}
return errors
}
private fun isValidEmail(email: String): Boolean {
return email.contains("@") && email.contains(".")
}
private fun isValidUrl(url: String): Boolean {
return url.startsWith("http://") || url.startsWith("https://")
}
}
@@ -0,0 +1,220 @@
package at.mocode.members.application.usecase
import at.mocode.dto.base.ApiResponse
import at.mocode.dto.base.ErrorDto
import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import com.benasher44.uuid.Uuid
/**
* Use case for retrieving person information from the member management context.
*
* This use case handles the business logic for person retrieval including:
* - Finding persons by ID or OEPS Satznummer
* - Searching persons by name
* - Retrieving persons by club membership
* - Listing active persons with pagination
*/
class GetPersonUseCase(
private val personRepository: PersonRepository
) {
/**
* Request data for getting a person by ID.
*/
data class GetPersonByIdRequest(
val personId: Uuid
)
/**
* Request data for getting a person by OEPS Satznummer.
*/
data class GetPersonByOepsSatzNrRequest(
val oepsSatzNr: String
)
/**
* Request data for searching persons by name.
*/
data class SearchPersonsByNameRequest(
val searchTerm: String,
val limit: Int = 50
)
/**
* Request data for getting persons by club.
*/
data class GetPersonsByClubRequest(
val vereinId: Uuid
)
/**
* Request data for listing active persons.
*/
data class ListActivePersonsRequest(
val limit: Int = 50,
val offset: Int = 0
)
/**
* Response data for person retrieval operations.
*/
data class GetPersonResponse(
val person: DomPerson
)
/**
* Response data for person list operations.
*/
data class GetPersonsResponse(
val persons: List<DomPerson>,
val total: Long? = null
)
/**
* Gets a person by their unique ID.
*/
suspend fun getById(request: GetPersonByIdRequest): ApiResponse<GetPersonResponse> {
return try {
val person = personRepository.findById(request.personId)
if (person != null) {
ApiResponse(
success = true,
data = GetPersonResponse(person)
)
} else {
ApiResponse(
success = false,
error = ErrorDto(
code = "PERSON_NOT_FOUND",
message = "Person with ID ${request.personId} not found"
)
)
}
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving the person: ${e.message}"
)
)
}
}
/**
* Gets a person by their OEPS Satznummer.
*/
suspend fun getByOepsSatzNr(request: GetPersonByOepsSatzNrRequest): ApiResponse<GetPersonResponse> {
return try {
if (request.oepsSatzNr.length != 6) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_OEPS_SATZNR",
message = "OEPS Satznummer must be exactly 6 digits"
)
)
}
val person = personRepository.findByOepsSatzNr(request.oepsSatzNr)
if (person != null) {
ApiResponse(
success = true,
data = GetPersonResponse(person)
)
} else {
ApiResponse(
success = false,
error = ErrorDto(
code = "PERSON_NOT_FOUND",
message = "Person with OEPS Satznummer ${request.oepsSatzNr} not found"
)
)
}
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving the person: ${e.message}"
)
)
}
}
/**
* Searches persons by name (first name or last name).
*/
suspend fun searchByName(request: SearchPersonsByNameRequest): ApiResponse<GetPersonsResponse> {
return try {
if (request.searchTerm.isBlank()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_SEARCH_TERM",
message = "Search term cannot be empty"
)
)
}
val persons = personRepository.findByName(request.searchTerm, request.limit)
ApiResponse(
success = true,
data = GetPersonsResponse(persons)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while searching persons: ${e.message}"
)
)
}
}
/**
* Gets all persons belonging to a specific club.
*/
suspend fun getByClub(request: GetPersonsByClubRequest): ApiResponse<GetPersonsResponse> {
return try {
val persons = personRepository.findByStammVereinId(request.vereinId)
ApiResponse(
success = true,
data = GetPersonsResponse(persons)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving club members: ${e.message}"
)
)
}
}
/**
* Lists active persons with pagination.
*/
suspend fun listActive(request: ListActivePersonsRequest): ApiResponse<GetPersonsResponse> {
return try {
val persons = personRepository.findAllActive(request.limit, request.offset)
val total = if (request.offset == 0) personRepository.countActive() else null
ApiResponse(
success = true,
data = GetPersonsResponse(persons, total)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while listing active persons: ${e.message}"
)
)
}
}
}
@@ -0,0 +1,287 @@
package at.mocode.members.application.usecase
import at.mocode.dto.base.ApiResponse
import at.mocode.dto.base.ErrorDto
import at.mocode.members.domain.model.DomVerein
import at.mocode.members.domain.repository.VereinRepository
import com.benasher44.uuid.Uuid
/**
* Use case for retrieving club/association information from the member management context.
*
* This use case handles the business logic for club retrieval including:
* - Finding clubs by ID or OEPS Vereinsnummer
* - Searching clubs by name
* - Retrieving clubs by location or geographic region
* - Listing active clubs with pagination
*/
class GetVereinUseCase(
private val vereinRepository: VereinRepository
) {
/**
* Request data for getting a club by ID.
*/
data class GetVereinByIdRequest(
val vereinId: Uuid
)
/**
* Request data for getting a club by OEPS Vereinsnummer.
*/
data class GetVereinByOepsVereinsNrRequest(
val oepsVereinsNr: String
)
/**
* Request data for searching clubs by name.
*/
data class SearchVereinsByNameRequest(
val searchTerm: String,
val limit: Int = 50
)
/**
* Request data for getting clubs by Bundesland.
*/
data class GetVereineByBundeslandRequest(
val bundeslandId: Uuid
)
/**
* Request data for getting clubs by country.
*/
data class GetVereineByLandRequest(
val landId: Uuid
)
/**
* Request data for searching clubs by location.
*/
data class SearchVereineByLocationRequest(
val searchTerm: String,
val limit: Int = 50
)
/**
* Request data for listing active clubs.
*/
data class ListActiveVereineRequest(
val limit: Int = 50,
val offset: Int = 0
)
/**
* Response data for club retrieval operations.
*/
data class GetVereinResponse(
val verein: DomVerein
)
/**
* Response data for club list operations.
*/
data class GetVereineResponse(
val vereine: List<DomVerein>,
val total: Long? = null
)
/**
* Gets a club by its unique ID.
*/
suspend fun getById(request: GetVereinByIdRequest): ApiResponse<GetVereinResponse> {
return try {
val verein = vereinRepository.findById(request.vereinId)
if (verein != null) {
ApiResponse(
success = true,
data = GetVereinResponse(verein)
)
} else {
ApiResponse(
success = false,
error = ErrorDto(
code = "VEREIN_NOT_FOUND",
message = "Club with ID ${request.vereinId} not found"
)
)
}
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving the club: ${e.message}"
)
)
}
}
/**
* Gets a club by its OEPS Vereinsnummer.
*/
suspend fun getByOepsVereinsNr(request: GetVereinByOepsVereinsNrRequest): ApiResponse<GetVereinResponse> {
return try {
if (request.oepsVereinsNr.length != 4) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_OEPS_VEREINSNR",
message = "OEPS Vereinsnummer must be exactly 4 digits"
)
)
}
val verein = vereinRepository.findByOepsVereinsNr(request.oepsVereinsNr)
if (verein != null) {
ApiResponse(
success = true,
data = GetVereinResponse(verein)
)
} else {
ApiResponse(
success = false,
error = ErrorDto(
code = "VEREIN_NOT_FOUND",
message = "Club with OEPS Vereinsnummer ${request.oepsVereinsNr} not found"
)
)
}
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving the club: ${e.message}"
)
)
}
}
/**
* Searches clubs by name or abbreviation.
*/
suspend fun searchByName(request: SearchVereinsByNameRequest): ApiResponse<GetVereineResponse> {
return try {
if (request.searchTerm.isBlank()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_SEARCH_TERM",
message = "Search term cannot be empty"
)
)
}
val vereine = vereinRepository.findByName(request.searchTerm, request.limit)
ApiResponse(
success = true,
data = GetVereineResponse(vereine)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while searching clubs: ${e.message}"
)
)
}
}
/**
* Gets all clubs in a specific Bundesland.
*/
suspend fun getByBundesland(request: GetVereineByBundeslandRequest): ApiResponse<GetVereineResponse> {
return try {
val vereine = vereinRepository.findByBundeslandId(request.bundeslandId)
ApiResponse(
success = true,
data = GetVereineResponse(vereine)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving clubs by Bundesland: ${e.message}"
)
)
}
}
/**
* Gets all clubs in a specific country.
*/
suspend fun getByLand(request: GetVereineByLandRequest): ApiResponse<GetVereineResponse> {
return try {
val vereine = vereinRepository.findByLandId(request.landId)
ApiResponse(
success = true,
data = GetVereineResponse(vereine)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while retrieving clubs by country: ${e.message}"
)
)
}
}
/**
* Searches clubs by location (city or postal code).
*/
suspend fun searchByLocation(request: SearchVereineByLocationRequest): ApiResponse<GetVereineResponse> {
return try {
if (request.searchTerm.isBlank()) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_SEARCH_TERM",
message = "Search term cannot be empty"
)
)
}
val vereine = vereinRepository.findByLocation(request.searchTerm, request.limit)
ApiResponse(
success = true,
data = GetVereineResponse(vereine)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while searching clubs by location: ${e.message}"
)
)
}
}
/**
* Lists active clubs with pagination.
*/
suspend fun listActive(request: ListActiveVereineRequest): ApiResponse<GetVereineResponse> {
return try {
val vereine = vereinRepository.findAllActive(request.limit, request.offset)
val total = if (request.offset == 0) vereinRepository.countActive() else null
ApiResponse(
success = true,
data = GetVereineResponse(vereine, total)
)
} catch (e: Exception) {
ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while listing active clubs: ${e.message}"
)
)
}
}
}
@@ -0,0 +1,100 @@
package at.mocode.members.domain.model
import at.mocode.enums.DatenQuelleE
import at.mocode.enums.GeschlechtE
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
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.datetime.LocalDate
import kotlinx.serialization.Serializable
/**
* Repräsentiert eine Person (Reiter, Funktionär, Kontaktperson etc.)
* im Domänenmodell der Anwendung.
*
* Die Daten können aus dem OEPS ZNS-Import (`Person_ZNS_Staging`) stammen
* oder manuell im System angelegt werden.
*
* @property personId Eindeutiger interner Identifikator für diese Person (UUID).
* @property oepsSatzNr Die offizielle 6-stellige OEPS-Satznummer der Person, falls vorhanden. Eindeutig.
* @property nachname Familienname der Person.
* @property vorname Vorname der Person.
* @property titel Akademischer Titel oder Anrede (z.B. Dr., Ing.).
* @property geburtsdatum Geburtsdatum der Person.
* @property geschlechtE Geschlecht der Person.
* @property nationalitaetLandId Fremdschlüssel zur `LandDefinition` für die Nationalität.
* @property feiId Optionale FEI-Identifikationsnummer der Person.
* @property telefon Private oder geschäftliche Telefonnummer.
* @property email Private oder geschäftliche E-Mail-Adresse.
* @property strasse Straße und Hausnummer der Hauptadresse.
* @property plz Postleitzahl der Hauptadresse.
* @property ort Ortschaft der Hauptadresse.
* @property adresszusatzZusatzinfo Weitere Adressinformationen.
* @property stammVereinId Optionale Verknüpfung zum `DomVerein` (Stammverein der Person).
* @property mitgliedsNummerBeiStammVerein Mitgliedsnummer der Person beim Stammverein.
* @property istGesperrt Gibt an, ob die Person laut OEPS oder intern gesperrt ist.
* @property sperrGrund Begründung für eine eventuelle Sperre.
* @property altersklasseOepsCodeRaw Der Roh-Code für die Altersklasse aus dem ZNS-Import (z.B. "JG", "JR", "25").
* Dient zur Ableitung oder als Information.
* @property istJungerReiterOepsFlag Ob die Person im ZNS als "Junger Reiter" ("Y") gekennzeichnet ist.
* @property kaderStatusOepsRaw Kaderkennzeichen aus dem ZNS-Import.
* @property datenQuelle Gibt die Herkunft dieses Datensatzes an (z.B. OEPS_ZNS, MANUELL).
* @property istAktiv Gibt an, ob dieser Personendatensatz aktuell aktiv ist.
* @property notizenIntern Interne Anmerkungen oder Notizen zu dieser Person.
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
*/
@Serializable
data class DomPerson(
@Serializable(with = UuidSerializer::class)
val personId: Uuid = uuid4(),
var oepsSatzNr: String?, // Wird aus Person_ZNS_Staging.oepsSatzNrPerson befüllt, UNIQUE
var nachname: String, // Wird aus Person_ZNS_Staging.familiennameRoh befüllt
var vorname: String, // Wird aus Person_ZNS_Staging.vornameRoh befüllt
var titel: String? = null, // Manuelle Eingabe ggf. später ZNS, falls vorhanden
@Serializable(with = KotlinLocalDateSerializer::class)
var geburtsdatum: LocalDate? = null, // Konvertiert aus Person_ZNS_Staging.geburtsdatumTextRoh
var geschlechtE: GeschlechtE? = null, // Konvertiert aus Person_ZNS_Staging.geschlechtCodeRoh
@Serializable(with = UuidSerializer::class)
var nationalitaetLandId: Uuid? = null, // Aufgelöst aus Person_ZNS_Staging.nationalitaetCodeRoh via LandDefinition
var feiId: String? = null, // Wird aus Person_ZNS_Staging.feiIdPersonRoh befüllt
var telefon: String? = null, // Wird aus Person_ZNS_Staging.telefonRoh befüllt
var email: String? = null, // Manuelle Eingabe, nicht in LIZENZ01.dat
// Adresse (manuelle Eingabe, nicht primär in LIZENZ01.dat für Person direkt)
var strasse: String? = null,
var plz: String? = null,
var ort: String? = null,
var adresszusatzZusatzinfo: String? = null,
@Serializable(with = UuidSerializer::class)
var stammVereinId: Uuid? = null, // Aufgelöst aus Person_ZNS_Staging.vereinsnameOepsRoh & bundeslandCodeOepsRoh via DomVerein
var mitgliedsNummerBeiStammVerein: String? = null, // Wird aus Person_ZNS_Staging.mitgliedNrVereinRoh befüllt
var istGesperrt: Boolean = false, // Konvertiert aus Person_ZNS_Staging.sperrlisteFlagOepsRoh ("S" -> true)
var sperrGrund: String? = null, // Manuelle Eingabe
var altersklasseOepsCodeRaw: String? = null, // Speichert Roh-Code "JG", "JR", "25"
var istJungerReiterOepsFlag: Boolean = false, // true wenn Roh-Code "Y"
var kaderStatusOepsRaw: String? = null, // Speichert Roh-Code (aktuell meist BLANK)
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
var istAktiv: Boolean = true,
var notizenIntern: String? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,70 @@
package at.mocode.members.domain.model
import at.mocode.enums.DatenQuelleE
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
/**
* Repräsentiert einen Reitverein im Domänenmodell der Anwendung.
*
* Die Daten für einen Verein können aus dem OEPS ZNS-Import (`Verein_ZNS_Staging`)
* stammen oder manuell im System angelegt werden (z.B. für ausländische Vereine).
* Jeder Verein wird durch eine systeminterne UUID und die offizielle OEPS-Vereinsnummer
* (falls vorhanden) eindeutig identifiziert.
*
* @property vereinId Eindeutiger interner Identifikator für diesen Verein (UUID).
* @property oepsVereinsNr Die offizielle 4-stellige OEPS-Vereinsnummer. Sollte eindeutig sein, falls vorhanden.
* @property name Der offizielle Name des Vereins.
* @property kuerzel Ein optionales Kürzel oder eine Kurzbezeichnung für den Verein.
* @property adresseStrasse Straße und Hausnummer des Vereinssitzes.
* @property plz Postleitzahl des Vereinssitzes.
* @property ort Ortschaft des Vereinssitzes.
* @property bundeslandId Optionale Verknüpfung zur `BundeslandDefinition`. Für OEPS-Vereine
* wird versucht, dies aus der ersten Ziffer der `oepsVereinsNr` abzuleiten.
* @property landId Verknüpfung zur `LandDefinition`. Für OEPS-Vereine ist dies "Österreich".
* @property emailAllgemein Allgemeine E-Mail-Adresse des Vereins.
* @property telefonAllgemein Allgemeine Telefonnummer des Vereins.
* @property webseiteUrl URL zur Webseite des Vereins.
* @property datenQuelle Gibt die Herkunft dieses Datensatzes an (z.B. OEPS_ZNS, MANUELL).
* @property istAktiv Gibt an, ob dieser Verein aktuell aktiv ist und im System verwendet werden kann.
* @property notizenIntern Interne Anmerkungen oder Notizen zu diesem Verein.
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
*/
@Serializable
data class DomVerein(
@Serializable(with = UuidSerializer::class)
val vereinId: Uuid = uuid4(),
var oepsVereinsNr: String?, // Kann null sein für nicht-OEPS Vereine. Wenn gesetzt, erste Ziffer = Bundesland-Code.
var name: String,
var kuerzel: String? = null,
var adresseStrasse: String? = null,
var plz: String? = null,
var ort: String? = null,
@Serializable(with = UuidSerializer::class)
var bundeslandId: Uuid? = null, // FK zu BundeslandDefinition.bundeslandId
@Serializable(with = UuidSerializer::class)
var landId: Uuid, // FK zu LandDefinition.landId (jeder Verein ist in einem Land)
var emailAllgemein: String? = null,
var telefonAllgemein: String? = null,
var webseiteUrl: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.OEPS_ZNS, // default OEPS_ZNS
var istAktiv: Boolean = true,
var notizenIntern: String? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,88 @@
package at.mocode.members.domain.repository
import at.mocode.members.domain.model.DomPerson
import com.benasher44.uuid.Uuid
/**
* Repository interface for Person domain operations.
*
* This interface defines the contract for person 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 PersonRepository {
/**
* Finds a person by their unique ID.
*
* @param id The unique identifier of the person
* @return The person if found, null otherwise
*/
suspend fun findById(id: Uuid): DomPerson?
/**
* Finds a person by their OEPS Satznummer.
*
* @param oepsSatzNr The OEPS Satznummer (6-digit identifier)
* @return The person if found, null otherwise
*/
suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson?
/**
* Finds all persons belonging to a specific club.
*
* @param vereinId The unique identifier of the club
* @return List of persons belonging to the club
*/
suspend fun findByStammVereinId(vereinId: Uuid): List<DomPerson>
/**
* Finds persons by name (partial match on first name or last name).
*
* @param searchTerm The search term to match against names
* @param limit Maximum number of results to return
* @return List of matching persons
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomPerson>
/**
* Finds all active persons.
*
* @param limit Maximum number of results to return
* @param offset Number of records to skip for pagination
* @return List of active persons
*/
suspend fun findAllActive(limit: Int = 50, offset: Int = 0): List<DomPerson>
/**
* Saves a person (create or update).
*
* @param person The person to save
* @return The saved person with updated timestamps
*/
suspend fun save(person: DomPerson): DomPerson
/**
* Deletes a person by ID.
*
* @param id The unique identifier of the person to delete
* @return true if the person was deleted, false if not found
*/
suspend fun delete(id: Uuid): Boolean
/**
* Checks if a person with the given OEPS Satznummer exists.
*
* @param oepsSatzNr The OEPS Satznummer to check
* @return true if a person with this number exists, false otherwise
*/
suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean
/**
* Counts the total number of active persons.
*
* @return The total count of active persons
*/
suspend fun countActive(): Long
}
@@ -0,0 +1,113 @@
package at.mocode.members.domain.repository
import at.mocode.members.domain.model.DomVerein
import com.benasher44.uuid.Uuid
/**
* Repository interface for Verein (Club/Association) domain operations.
*
* This interface defines the contract for club 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 VereinRepository {
/**
* Finds a club by its unique ID.
*
* @param id The unique identifier of the club
* @return The club if found, null otherwise
*/
suspend fun findById(id: Uuid): DomVerein?
/**
* Finds a club by its OEPS Vereinsnummer.
*
* @param oepsVereinsNr The OEPS Vereinsnummer (4-digit identifier)
* @return The club if found, null otherwise
*/
suspend fun findByOepsVereinsNr(oepsVereinsNr: String): DomVerein?
/**
* Finds clubs by name (partial match).
*
* @param searchTerm The search term to match against club names
* @param limit Maximum number of results to return
* @return List of matching clubs
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomVerein>
/**
* Finds all clubs in a specific Bundesland (state).
*
* @param bundeslandId The unique identifier of the Bundesland
* @return List of clubs in the specified Bundesland
*/
suspend fun findByBundeslandId(bundeslandId: Uuid): List<DomVerein>
/**
* Finds all clubs in a specific country.
*
* @param landId The unique identifier of the country
* @return List of clubs in the specified country
*/
suspend fun findByLandId(landId: Uuid): List<DomVerein>
/**
* Finds all active clubs.
*
* @param limit Maximum number of results to return
* @param offset Number of records to skip for pagination
* @return List of active clubs
*/
suspend fun findAllActive(limit: Int = 50, offset: Int = 0): List<DomVerein>
/**
* Finds clubs by location (city/postal code).
*
* @param searchTerm The search term to match against city or postal code
* @param limit Maximum number of results to return
* @return List of matching clubs
*/
suspend fun findByLocation(searchTerm: String, limit: Int = 50): List<DomVerein>
/**
* Saves a club (create or update).
*
* @param verein The club to save
* @return The saved club with updated timestamps
*/
suspend fun save(verein: DomVerein): DomVerein
/**
* Deletes a club by ID.
*
* @param id The unique identifier of the club to delete
* @return true if the club was deleted, false if not found
*/
suspend fun delete(id: Uuid): Boolean
/**
* Checks if a club with the given OEPS Vereinsnummer exists.
*
* @param oepsVereinsNr The OEPS Vereinsnummer to check
* @return true if a club with this number exists, false otherwise
*/
suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean
/**
* Counts the total number of active clubs.
*
* @return The total count of active clubs
*/
suspend fun countActive(): Long
/**
* Counts the number of active clubs in a specific Bundesland.
*
* @param bundeslandId The unique identifier of the Bundesland
* @return The count of active clubs in the specified Bundesland
*/
suspend fun countActiveByBundeslandId(bundeslandId: Uuid): Long
}
@@ -0,0 +1,79 @@
package at.mocode.members.domain.service
import com.benasher44.uuid.Uuid
/**
* Service interface for accessing master data from other bounded contexts.
*
* This interface abstracts the communication with the master-data context,
* following the Self-Contained Systems architecture principles by avoiding
* direct repository dependencies between bounded contexts.
*/
interface MasterDataService {
/**
* Data class representing country information.
*/
data class CountryInfo(
val id: Uuid,
val name: String,
val code: String
)
/**
* Data class representing state/bundesland information.
*/
data class StateInfo(
val id: Uuid,
val name: String,
val code: String,
val countryId: Uuid
)
/**
* Validates if a country exists by its ID.
*
* @param countryId The unique identifier of the country
* @return true if the country exists, false otherwise
*/
suspend fun countryExists(countryId: Uuid): Boolean
/**
* Validates if a state/bundesland exists by its ID.
*
* @param stateId The unique identifier of the state
* @return true if the state exists, false otherwise
*/
suspend fun stateExists(stateId: Uuid): Boolean
/**
* Gets country information by ID.
*
* @param countryId The unique identifier of the country
* @return CountryInfo if found, null otherwise
*/
suspend fun getCountryById(countryId: Uuid): CountryInfo?
/**
* Gets state information by ID.
*
* @param stateId The unique identifier of the state
* @return StateInfo if found, null otherwise
*/
suspend fun getStateById(stateId: Uuid): StateInfo?
/**
* Gets all available countries.
*
* @return List of all countries
*/
suspend fun getAllCountries(): List<CountryInfo>
/**
* Gets all states for a specific country.
*
* @param countryId The unique identifier of the country
* @return List of states in the specified country
*/
suspend fun getStatesByCountry(countryId: Uuid): List<StateInfo>
}
@@ -0,0 +1,140 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.toKotlinInstant
import kotlinx.datetime.toKotlinLocalDate
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
/**
* Exposed-based implementation of PersonRepository.
*
* This implementation provides data persistence for Person entities
* using the Exposed SQL framework and PostgreSQL database.
*/
class PersonRepositoryImpl : PersonRepository {
override suspend fun findById(id: Uuid): DomPerson? {
return PersonTable.select { PersonTable.id eq id }
.map { rowToDomPerson(it) }
.singleOrNull()
}
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson? {
return PersonTable.select { PersonTable.oepsSatzNr eq oepsSatzNr }
.map { rowToDomPerson(it) }
.singleOrNull()
}
override suspend fun findByStammVereinId(vereinId: Uuid): List<DomPerson> {
return PersonTable.select { PersonTable.stammVereinId eq vereinId }
.map { rowToDomPerson(it) }
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPerson> {
val searchPattern = "%$searchTerm%"
return PersonTable.select {
(PersonTable.nachname like searchPattern) or
(PersonTable.vorname like searchPattern)
}
.limit(limit)
.map { rowToDomPerson(it) }
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomPerson> {
return PersonTable.select { PersonTable.istAktiv eq true }
.limit(limit, offset.toLong())
.map { rowToDomPerson(it) }
}
override suspend fun save(person: DomPerson): DomPerson {
val now = Clock.System.now()
val updatedPerson = person.copy(updatedAt = now)
PersonTable.insertOrUpdate(PersonTable.id) {
it[id] = person.personId
it[oepsSatzNr] = person.oepsSatzNr
it[nachname] = person.nachname
it[vorname] = person.vorname
it[titel] = person.titel
it[geburtsdatum] = person.geburtsdatum
it[geschlecht] = person.geschlechtE
it[nationalitaetLandId] = person.nationalitaetLandId
it[feiId] = person.feiId
it[telefon] = person.telefon
it[email] = person.email
it[strasse] = person.strasse
it[plz] = person.plz
it[ort] = person.ort
it[adresszusatzZusatzinfo] = person.adresszusatzZusatzinfo
it[stammVereinId] = person.stammVereinId
it[mitgliedsNummerBeiStammVerein] = person.mitgliedsNummerBeiStammVerein
it[istGesperrt] = person.istGesperrt
it[sperrGrund] = person.sperrGrund
it[altersklasseOepsCodeRaw] = person.altersklasseOepsCodeRaw
it[istJungerReiterOepsFlag] = person.istJungerReiterOepsFlag
it[kaderStatusOepsRaw] = person.kaderStatusOepsRaw
it[datenQuelle] = person.datenQuelle
it[istAktiv] = person.istAktiv
it[notizenIntern] = person.notizenIntern
it[createdAt] = person.createdAt.toJavaInstant()
it[updatedAt] = updatedPerson.updatedAt.toJavaInstant()
}
return updatedPerson
}
override suspend fun delete(id: Uuid): Boolean {
val deletedRows = PersonTable.deleteWhere { PersonTable.id eq id }
return deletedRows > 0
}
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
return PersonTable.select { PersonTable.oepsSatzNr eq oepsSatzNr }
.count() > 0
}
override suspend fun countActive(): Long {
return PersonTable.select { PersonTable.istAktiv eq true }
.count()
}
/**
* Converts a database row to a DomPerson domain object.
*/
private fun rowToDomPerson(row: ResultRow): DomPerson {
return DomPerson(
personId = row[PersonTable.id].value,
oepsSatzNr = row[PersonTable.oepsSatzNr],
nachname = row[PersonTable.nachname],
vorname = row[PersonTable.vorname],
titel = row[PersonTable.titel],
geburtsdatum = row[PersonTable.geburtsdatum],
geschlechtE = row[PersonTable.geschlecht],
nationalitaetLandId = row[PersonTable.nationalitaetLandId],
feiId = row[PersonTable.feiId],
telefon = row[PersonTable.telefon],
email = row[PersonTable.email],
strasse = row[PersonTable.strasse],
plz = row[PersonTable.plz],
ort = row[PersonTable.ort],
adresszusatzZusatzinfo = row[PersonTable.adresszusatzZusatzinfo],
stammVereinId = row[PersonTable.stammVereinId],
mitgliedsNummerBeiStammVerein = row[PersonTable.mitgliedsNummerBeiStammVerein],
istGesperrt = row[PersonTable.istGesperrt],
sperrGrund = row[PersonTable.sperrGrund],
altersklasseOepsCodeRaw = row[PersonTable.altersklasseOepsCodeRaw],
istJungerReiterOepsFlag = row[PersonTable.istJungerReiterOepsFlag],
kaderStatusOepsRaw = row[PersonTable.kaderStatusOepsRaw],
datenQuelle = row[PersonTable.datenQuelle],
istAktiv = row[PersonTable.istAktiv],
notizenIntern = row[PersonTable.notizenIntern],
createdAt = row[PersonTable.createdAt].toKotlinInstant(),
updatedAt = row[PersonTable.updatedAt].toKotlinInstant()
)
}
}
@@ -0,0 +1,60 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.DatenQuelleE
import at.mocode.enums.GeschlechtE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.date
/**
* Exposed table definition for Person entities.
*
* This table represents the database schema for storing person data
* in the member management bounded context.
*/
object PersonTable : UUIDTable("persons") {
// Basic person information
val oepsSatzNr = varchar("oeps_satz_nr", 6).nullable().uniqueIndex()
val nachname = varchar("nachname", 100)
val vorname = varchar("vorname", 100)
val titel = varchar("titel", 50).nullable()
// Personal details
val geburtsdatum = date("geburtsdatum").nullable()
val geschlecht = enumerationByName("geschlecht", 10, GeschlechtE::class).nullable()
val nationalitaetLandId = uuid("nationalitaet_land_id").nullable()
val feiId = varchar("fei_id", 20).nullable()
// Contact information
val telefon = varchar("telefon", 50).nullable()
val email = varchar("email", 100).nullable()
// Address information
val strasse = varchar("strasse", 200).nullable()
val plz = varchar("plz", 10).nullable()
val ort = varchar("ort", 100).nullable()
val adresszusatzZusatzinfo = varchar("adresszusatz_zusatzinfo", 200).nullable()
// Club membership
val stammVereinId = uuid("stamm_verein_id").nullable()
val mitgliedsNummerBeiStammVerein = varchar("mitglieds_nummer_bei_stamm_verein", 50).nullable()
// Status and restrictions
val istGesperrt = bool("ist_gesperrt").default(false)
val sperrGrund = varchar("sperr_grund", 500).nullable()
// OEPS specific data
val altersklasseOepsCodeRaw = varchar("altersklasse_oeps_code_raw", 10).nullable()
val istJungerReiterOepsFlag = bool("ist_junger_reiter_oeps_flag").default(false)
val kaderStatusOepsRaw = varchar("kader_status_oeps_raw", 10).nullable()
// Metadata
val datenQuelle = enumerationByName("daten_quelle", 20, DatenQuelleE::class).default(DatenQuelleE.MANUELL)
val istAktiv = bool("ist_aktiv").default(true)
val notizenIntern = text("notizen_intern").nullable()
// Audit fields
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
}
@@ -0,0 +1,141 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomVerein
import at.mocode.members.domain.repository.VereinRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.toKotlinInstant
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
/**
* Exposed-based implementation of VereinRepository.
*
* This implementation provides data persistence for Verein (Club/Association) entities
* using the Exposed SQL framework and PostgreSQL database.
*/
class VereinRepositoryImpl : VereinRepository {
override suspend fun findById(id: Uuid): DomVerein? {
return VereinTable.select { VereinTable.id eq id }
.map { rowToDomVerein(it) }
.singleOrNull()
}
override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): DomVerein? {
return VereinTable.select { VereinTable.oepsVereinsNr eq oepsVereinsNr }
.map { rowToDomVerein(it) }
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> {
val searchPattern = "%$searchTerm%"
return VereinTable.select {
(VereinTable.name like searchPattern) or
(VereinTable.kuerzel like searchPattern)
}
.limit(limit)
.map { rowToDomVerein(it) }
}
override suspend fun findByBundeslandId(bundeslandId: Uuid): List<DomVerein> {
return VereinTable.select { VereinTable.bundeslandId eq bundeslandId }
.map { rowToDomVerein(it) }
}
override suspend fun findByLandId(landId: Uuid): List<DomVerein> {
return VereinTable.select { VereinTable.landId eq landId }
.map { rowToDomVerein(it) }
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> {
return VereinTable.select { VereinTable.istAktiv eq true }
.limit(limit, offset.toLong())
.map { rowToDomVerein(it) }
}
override suspend fun findByLocation(searchTerm: String, limit: Int): List<DomVerein> {
val searchPattern = "%$searchTerm%"
return VereinTable.select {
(VereinTable.ort like searchPattern) or
(VereinTable.plz like searchPattern)
}
.limit(limit)
.map { rowToDomVerein(it) }
}
override suspend fun save(verein: DomVerein): DomVerein {
val now = Clock.System.now()
val updatedVerein = verein.copy(updatedAt = now)
VereinTable.insertOrUpdate(VereinTable.id) {
it[id] = verein.vereinId
it[oepsVereinsNr] = verein.oepsVereinsNr
it[name] = verein.name
it[kuerzel] = verein.kuerzel
it[adresseStrasse] = verein.adresseStrasse
it[plz] = verein.plz
it[ort] = verein.ort
it[bundeslandId] = verein.bundeslandId
it[landId] = verein.landId
it[emailAllgemein] = verein.emailAllgemein
it[telefonAllgemein] = verein.telefonAllgemein
it[webseiteUrl] = verein.webseiteUrl
it[datenQuelle] = verein.datenQuelle
it[istAktiv] = verein.istAktiv
it[notizenIntern] = verein.notizenIntern
it[createdAt] = verein.createdAt.toJavaInstant()
it[updatedAt] = updatedVerein.updatedAt.toJavaInstant()
}
return updatedVerein
}
override suspend fun delete(id: Uuid): Boolean {
val deletedRows = VereinTable.deleteWhere { VereinTable.id eq id }
return deletedRows > 0
}
override suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean {
return VereinTable.select { VereinTable.oepsVereinsNr eq oepsVereinsNr }
.count() > 0
}
override suspend fun countActive(): Long {
return VereinTable.select { VereinTable.istAktiv eq true }
.count()
}
override suspend fun countActiveByBundeslandId(bundeslandId: Uuid): Long {
return VereinTable.select {
(VereinTable.istAktiv eq true) and (VereinTable.bundeslandId eq bundeslandId)
}
.count()
}
/**
* Converts a database row to a DomVerein domain object.
*/
private fun rowToDomVerein(row: ResultRow): DomVerein {
return DomVerein(
vereinId = row[VereinTable.id].value,
oepsVereinsNr = row[VereinTable.oepsVereinsNr],
name = row[VereinTable.name],
kuerzel = row[VereinTable.kuerzel],
adresseStrasse = row[VereinTable.adresseStrasse],
plz = row[VereinTable.plz],
ort = row[VereinTable.ort],
bundeslandId = row[VereinTable.bundeslandId],
landId = row[VereinTable.landId],
emailAllgemein = row[VereinTable.emailAllgemein],
telefonAllgemein = row[VereinTable.telefonAllgemein],
webseiteUrl = row[VereinTable.webseiteUrl],
datenQuelle = row[VereinTable.datenQuelle],
istAktiv = row[VereinTable.istAktiv],
notizenIntern = row[VereinTable.notizenIntern],
createdAt = row[VereinTable.createdAt].toKotlinInstant(),
updatedAt = row[VereinTable.updatedAt].toKotlinInstant()
)
}
}
@@ -0,0 +1,42 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.DatenQuelleE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Exposed table definition for Verein (Club/Association) entities.
*
* This table represents the database schema for storing club data
* in the member management bounded context.
*/
object VereinTable : UUIDTable("vereine") {
// Basic club information
val oepsVereinsNr = varchar("oeps_vereins_nr", 4).nullable().uniqueIndex()
val name = varchar("name", 200)
val kuerzel = varchar("kuerzel", 20).nullable()
// Address information
val adresseStrasse = varchar("adresse_strasse", 200).nullable()
val plz = varchar("plz", 10).nullable()
val ort = varchar("ort", 100).nullable()
// Geographic references
val bundeslandId = uuid("bundesland_id").nullable()
val landId = uuid("land_id")
// Contact information
val emailAllgemein = varchar("email_allgemein", 100).nullable()
val telefonAllgemein = varchar("telefon_allgemein", 50).nullable()
val webseiteUrl = varchar("webseite_url", 200).nullable()
// Metadata
val datenQuelle = enumerationByName("daten_quelle", 20, DatenQuelleE::class).default(DatenQuelleE.OEPS_ZNS)
val istAktiv = bool("ist_aktiv").default(true)
val notizenIntern = text("notizen_intern").nullable()
// Audit fields
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
}