diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt new file mode 100644 index 00000000..d21826ed --- /dev/null +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/KotlinxInstantSerializer.kt @@ -0,0 +1,26 @@ +@file:OptIn(kotlin.time.ExperimentalTime::class) +package at.mocode.core.domain.serialization + +import kotlinx.datetime.Instant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +/** + * Serializer for kotlinx.datetime.Instant. + * Uses ISO-8601 string representation. + */ +object KotlinxInstantSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KotlinxInstant", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } +} diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt index e3bb102c..e591a043 100644 --- a/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/serialization/Serializers.kt @@ -31,6 +31,8 @@ object KotlinInstantSerializer : KSerializer { } } +// Note: Serializer for kotlinx.datetime.Instant is defined in a separate file + /** * Serializer für UUID Objekte. * Konvertiert UUID zu/von String-Repräsentation. diff --git a/services/members/members-api/build.gradle.kts b/services/members/members-api/build.gradle.kts index c29ae742..3a9750d1 100644 --- a/services/members/members-api/build.gradle.kts +++ b/services/members/members-api/build.gradle.kts @@ -17,10 +17,8 @@ dependencies { implementation(projects.platform.platformDependencies) implementation(projects.services.members.membersDomain) - implementation(projects.services.members.membersApplication) implementation(projects.core.coreDomain) implementation(projects.core.coreUtils) - implementation(projects.infrastructure.messaging.messagingClient) implementation("org.springframework:spring-web") implementation("org.springdoc:springdoc-openapi-starter-common") diff --git a/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt b/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt index fa5fed37..ed2bceec 100644 --- a/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt +++ b/services/members/members-api/src/main/kotlin/at/mocode/members/api/rest/MemberController.kt @@ -2,98 +2,51 @@ package at.mocode.members.api.rest import at.mocode.core.domain.model.ApiResponse -import at.mocode.infrastructure.messaging.client.EventPublisher -import at.mocode.members.application.usecase.* +import at.mocode.members.domain.model.Member import at.mocode.members.domain.repository.MemberRepository -import kotlin.uuid.Uuid import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.Parameter -import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag import kotlinx.coroutines.runBlocking import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toKotlinLocalDate +import java.time.ZoneId +import java.time.LocalDate as JLocalDate import org.springframework.beans.factory.annotation.Qualifier import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.oauth2.jwt.Jwt -import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import kotlin.uuid.Uuid -/** - * Einfache No-op EventPublisher Implementierung für den Controller. - */ -class NoOpEventPublisher : EventPublisher { - override suspend fun publishEvent(topic: String, key: String?, event: Any) { - // No-op Implementierung - Events werden in dieser einfachen Version nicht veröffentlicht - } - - override suspend fun publishEvents(topic: String, events: List>) { - // No-op Implementierung - Events werden in dieser einfachen Version nicht veröffentlicht - } -} - -/** - * REST API Controller für Mitgliederverwaltungs-Operationen. - * - * Dieser Controller stellt HTTP-Endpunkte für alle mitgliederbezogenen Operationen - * zur Verfügung, einschließlich CRUD-Operationen und Mitgliedersuche. - */ @RestController @RequestMapping("/api/members") -@Tag(name = "Members", description = "Mitgliederverwaltungs-Operationen") +@Tag(name = "Members", description = "Mitgliederverwaltungs-Operationen (minimal)") class MemberController( @Qualifier("memberRepositoryImpl") private val memberRepository: MemberRepository ) { - // Einfache No-op EventPublisher Implementierung vorerst - private val eventPublisher = NoOpEventPublisher() + data class SyncResponse( + val ensured: Boolean, + val created: Boolean, + val memberId: String?, + val membershipNumber: String? + ) - private val createMemberUseCase = CreateMemberUseCase(memberRepository, eventPublisher) - private val getMemberUseCase = GetMemberUseCase(memberRepository) - private val updateMemberUseCase = UpdateMemberUseCase(memberRepository) - private val deleteMemberUseCase = DeleteMemberUseCase(memberRepository) - private val findExpiringMembershipsUseCase = FindExpiringMembershipsUseCase(memberRepository) - private val findMembersByDateRangeUseCase = FindMembersByDateRangeUseCase(memberRepository) - private val validateMemberDataUseCase = ValidateMemberDataUseCase(memberRepository) - private val ensureMemberProfileExistsUseCase = EnsureMemberProfileExistsUseCase(memberRepository, eventPublisher) + data class MemberProfileDto( + val id: String? = null, + val username: String? = null, + val email: String? = null, + val firstName: String? = null, + val lastName: String? = null, + val roles: List = emptyList() + ) - /** - * Hilfsmethode zur Behandlung gemeinsamer Antwortmuster für Use-Case-Ausführung - */ - private inline fun handleUseCaseExecution( - crossinline operation: suspend () -> ApiResponse, - successStatus: HttpStatus = HttpStatus.OK, - crossinline extractData: (T) -> Any = { it as Any } - ): ResponseEntity> { - return try { - val response = runBlocking { operation() } - - if (response.success && response.data != null) { - ResponseEntity.status(successStatus) - .body(ApiResponse.success(extractData(response.data!!))) - } else { - val statusCode = when (response.error?.code) { - "MEMBER_NOT_FOUND" -> HttpStatus.NOT_FOUND - "VALIDATION_ERROR" -> HttpStatus.BAD_REQUEST - else -> HttpStatus.BAD_REQUEST - } - ResponseEntity.status(statusCode) - .body(ApiResponse.error(response.error?.message ?: "Operation failed")) - } - } catch (e: IllegalArgumentException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Invalid input format: ${e.message}")) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Internal server error: ${e.message}")) - } - } - - // --------------------------------------------------------------------- - // Synchronisation nach Login: Stellt sicher, dass Member-Profil existiert - // --------------------------------------------------------------------- + // Synchronisiert/erstellt bei Bedarf ein Member-Profil basierend auf den JWT-Claims @Operation( summary = "Synchronisiert das Member-Profil für den eingeloggten Benutzer", description = "Erstellt bei Bedarf ein Mitglied basierend auf den JWT-Claims (mock OEPS fetch)" @@ -101,439 +54,108 @@ class MemberController( @PostMapping("/sync") fun syncMemberProfile( @AuthenticationPrincipal jwt: Jwt - ): ResponseEntity> = handleUseCaseExecution( - operation = { + ): ResponseEntity> { + return try { val sub = jwt.subject val email = jwt.getClaimAsString("email") val username = jwt.getClaimAsString("preferred_username") - val response = ensureMemberProfileExistsUseCase.execute( - EnsureMemberProfileExistsUseCase.Request( - userSub = sub, + + val ensured = runBlocking { ensureMemberProfileExists(sub, email, username) } + ResponseEntity.ok(ApiResponse.success(ensured)) + } catch (e: Exception) { + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("INTERNAL_ERROR", "Sync failed: ${e.message}")) + } + } + + // Liefert das Member-Profil des eingeloggten Benutzers + @Operation(summary = "Eigene Profilinformationen abrufen") + @GetMapping("/me") + fun getMyProfile( + @AuthenticationPrincipal jwt: Jwt + ): ResponseEntity> { + return try { + val sub = jwt.subject + val email = jwt.getClaimAsString("email") + val username = jwt.getClaimAsString("preferred_username") + + val member = runBlocking { + val lookupEmail = email ?: username?.let { "$it@local" } ?: "user-${sub.takeLast(8)}@local" + memberRepository.findByEmail(lookupEmail) + } + + val profile = if (member != null) { + MemberProfileDto( + id = member.memberId.toString(), + username = username, + email = member.email, + firstName = member.firstName, + lastName = member.lastName, + roles = emptyList() + ) + } else { + MemberProfileDto( + id = null, + username = username, email = email, - username = username + firstName = username?.substringBefore('@')?.replaceFirstChar { it.titlecase() }, + lastName = null, + roles = emptyList() ) - ) - ApiResponse.success(response) - }, - successStatus = HttpStatus.OK - ) { it } - - /** - * Hilfsmethode zur Behandlung von Repository-Operationen mit gemeinsamer Fehlerbehandlung - */ - private inline fun handleRepositoryOperation( - crossinline operation: () -> T, - errorMessage: String = "Operation failed" - ): ResponseEntity> { - return try { - val result = runBlocking { operation() } - ResponseEntity.ok(ApiResponse.success(result)) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("$errorMessage: ${e.message}")) - } - } - - /** - * Alle Mitglieder mit optionaler Filterung abrufen - */ - @Operation( - summary = "Alle Mitglieder abrufen", - description = "Abrufen aller Mitglieder mit optionaler Filterung nach Aktivitätsstatus und Suchbegriff" - ) - @ApiResponses( - value = [ - SwaggerApiResponse(responseCode = "200", description = "Mitglieder erfolgreich abgerufen"), - SwaggerApiResponse(responseCode = "500", description = "Interner Serverfehler") - ] - ) - @GetMapping - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getAllMembers( - @Parameter(description = "Nur nach aktiven Mitgliedern filtern", example = "true") - @RequestParam(defaultValue = "true") activeOnly: Boolean, - @Parameter(description = "Maximale Anzahl der zurückzugebenden Ergebnisse", example = "100") - @RequestParam(defaultValue = "100") limit: Int, - @Parameter(description = "Anzahl der zu überspringenden Ergebnisse", example = "0") - @RequestParam(defaultValue = "0") offset: Int, - @Parameter(description = "Suchbegriff für Mitgliedernamen") - @RequestParam(required = false) search: String? - ): ResponseEntity>> { - return try { - val members = runBlocking { - when { - search != null -> memberRepository.findByName(search, limit) - activeOnly -> memberRepository.findAllActive(limit, offset) - else -> memberRepository.findAll(limit, offset) - } } - ResponseEntity.ok(ApiResponse.success(members)) + + ResponseEntity.ok(ApiResponse.success(profile)) } catch (e: Exception) { ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error>("Failed to retrieve members: ${e.message}")) + .body(ApiResponse.error("INTERNAL_ERROR", "Failed to load profile: ${e.message}")) } } - /** - * Get member by ID - */ - @Operation( - summary = "Get member by ID", - description = "Retrieve a specific member by their unique identifier" - ) - @ApiResponses( - value = [ - SwaggerApiResponse(responseCode = "200", description = "Member found successfully"), - SwaggerApiResponse(responseCode = "400", description = "Invalid member ID format"), - SwaggerApiResponse(responseCode = "404", description = "Member not found"), - SwaggerApiResponse(responseCode = "500", description = "Internal server error") - ] - ) - @GetMapping("/{id}") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getMemberById( - @Parameter(description = "Member unique identifier", example = "123e4567-e89b-12d3-a456-426614174000") - @PathVariable id: String - ): ResponseEntity> { - return handleUseCaseExecution( - operation = { - val memberId = Uuid.parse(id) - val request = GetMemberUseCase.GetMemberRequest(memberId) - getMemberUseCase.execute(request) - }, - extractData = { (it as GetMemberUseCase.GetMemberResponse).member } + private suspend fun ensureMemberProfileExists(userSub: String, email: String?, username: String?): SyncResponse { + val lookupEmail = email ?: username?.let { "$it@local" } ?: "user-${userSub.takeLast(8)}@local" + val existing = memberRepository.findByEmail(lookupEmail) + if (existing != null) { + return SyncResponse( + ensured = true, + created = false, + memberId = existing.memberId.toString(), + membershipNumber = existing.membershipNumber + ) + } + + val (firstName, lastName) = mockFetchNames(userSub, email, username) + val today: LocalDate = JLocalDate.now(ZoneId.systemDefault()).toKotlinLocalDate() + val generatedMembershipNumber = "AUTO-${userSub.takeLast(8)}" + + val newMember = Member( + firstName = firstName, + lastName = lastName, + email = lookupEmail, + membershipNumber = generatedMembershipNumber, + membershipStartDate = today, + phone = null, + dateOfBirth = null, + membershipEndDate = null, + isActive = true, + address = null, + emergencyContact = null + ) + + val saved = memberRepository.save(newMember) + return SyncResponse( + ensured = true, + created = true, + memberId = saved.memberId.toString(), + membershipNumber = saved.membershipNumber ) } - /** - * Get member by membership number - */ - @GetMapping("/by-membership-number/{membershipNumber}") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getMemberByMembershipNumber(@PathVariable membershipNumber: String): ResponseEntity> { - return try { - val response = runBlocking { getMemberUseCase.getByMembershipNumber(membershipNumber) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success((response.data as GetMemberUseCase.GetMemberResponse).member)) - } else { - ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("Member not found")) - } - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to retrieve member: ${e.message}")) - } + private fun mockFetchNames(sub: String, email: String?, username: String?): Pair { + val source = username ?: email ?: sub + val base = source.substringBefore('@').replace(".", " ").trim().ifBlank { "Reiter" } + val parts = base.split(" ") + val first = parts.firstOrNull()?.replaceFirstChar { it.titlecase() } ?: "Reiter" + val last = parts.drop(1).joinToString(" ").ifBlank { "Unbekannt" }.replaceFirstChar { it.titlecase() } + return first to last } - - /** - * Get member by email - */ - @GetMapping("/by-email/{email}") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getMemberByEmail(@PathVariable email: String): ResponseEntity> { - return try { - val response = runBlocking { getMemberUseCase.getByEmail(email) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success((response.data as GetMemberUseCase.GetMemberResponse).member)) - } else { - ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error("Member not found")) - } - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to retrieve member: ${e.message}")) - } - } - - /** - * Get member statistics - */ - @GetMapping("/stats") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getMemberStats(): ResponseEntity> { - return try { - val activeCount = runBlocking { memberRepository.countActive() } - val totalCount = runBlocking { memberRepository.countAll() } - - val stats = MemberStats( - totalActive = activeCount, - totalMembers = totalCount - ) - - ResponseEntity.ok(ApiResponse.success(stats)) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to retrieve member statistics: ${e.message}")) - } - } - - /** - * Create new member - */ - @Operation( - summary = "Create new member", - description = "Create a new member with the provided information" - ) - @ApiResponses( - value = [ - SwaggerApiResponse(responseCode = "201", description = "Member created successfully"), - SwaggerApiResponse(responseCode = "400", description = "Invalid request data"), - SwaggerApiResponse(responseCode = "500", description = "Internal server error") - ] - ) - @PostMapping - @PreAuthorize("hasAuthority('PERSON_CREATE')") - fun createMember( - @Parameter(description = "Member creation request data") - @RequestBody createRequest: CreateMemberRequest - ): ResponseEntity> { - return handleUseCaseExecution( - operation = { - val useCaseRequest = CreateMemberUseCase.CreateMemberRequest( - firstName = createRequest.firstName, - lastName = createRequest.lastName, - email = createRequest.email, - phone = createRequest.phone, - dateOfBirth = createRequest.dateOfBirth, - membershipNumber = createRequest.membershipNumber, - membershipStartDate = createRequest.membershipStartDate, - membershipEndDate = createRequest.membershipEndDate, - isActive = createRequest.isActive, - address = createRequest.address, - emergencyContact = createRequest.emergencyContact - ) - createMemberUseCase.execute(useCaseRequest) - }, - successStatus = HttpStatus.CREATED, - extractData = { (it as CreateMemberUseCase.CreateMemberResponse).member } - ) - } - - /** - * Update member - */ - @PutMapping("/{id}") - @PreAuthorize("hasAuthority('PERSON_UPDATE')") - fun updateMember(@PathVariable id: String, @RequestBody updateRequest: UpdateMemberRequest): ResponseEntity> { - return try { - val memberId = Uuid.parse(id) - val useCaseRequest = UpdateMemberUseCase.UpdateMemberRequest( - memberId = memberId, - firstName = updateRequest.firstName, - lastName = updateRequest.lastName, - email = updateRequest.email, - phone = updateRequest.phone, - dateOfBirth = updateRequest.dateOfBirth, - membershipNumber = updateRequest.membershipNumber, - membershipStartDate = updateRequest.membershipStartDate, - membershipEndDate = updateRequest.membershipEndDate, - isActive = updateRequest.isActive, - address = updateRequest.address, - emergencyContact = updateRequest.emergencyContact - ) - - val response = runBlocking { updateMemberUseCase.execute(useCaseRequest) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success((response.data as UpdateMemberUseCase.UpdateMemberResponse).member)) - } else { - val statusCode = when (response.error?.code) { - "MEMBER_NOT_FOUND" -> HttpStatus.NOT_FOUND - else -> HttpStatus.BAD_REQUEST - } - ResponseEntity.status(statusCode) - .body(ApiResponse.error(response.error?.message ?: "Failed to update member")) - } - } catch (_: IllegalArgumentException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Invalid member ID format")) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to update member: ${e.message}")) - } - } - - /** - * Get members with expiring memberships - */ - @GetMapping("/expiring-memberships") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getExpiringMemberships( - @RequestParam(defaultValue = "30") daysAhead: Int - ): ResponseEntity> { - return try { - val request = FindExpiringMembershipsUseCase.FindExpiringMembershipsRequest(daysAhead) - val response = runBlocking { findExpiringMembershipsUseCase.execute(request) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success(response.data)) - } else { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error(response.error?.message ?: "Failed to find expiring memberships")) - } - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to find expiring memberships: ${e.message}")) - } - } - - /** - * Get members by date range - */ - @GetMapping("/by-date-range") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun getMembersByDateRange( - @RequestParam startDate: String, - @RequestParam endDate: String, - @RequestParam(defaultValue = "MEMBERSHIP_START_DATE") dateType: String - ): ResponseEntity> { - return try { - val startLocalDate = LocalDate.parse(startDate) - val endLocalDate = LocalDate.parse(endDate) - val dateRangeType = FindMembersByDateRangeUseCase.DateRangeType.valueOf(dateType) - - val request = FindMembersByDateRangeUseCase.FindMembersByDateRangeRequest( - startDate = startLocalDate, - endDate = endLocalDate, - dateType = dateRangeType - ) - val response = runBlocking { findMembersByDateRangeUseCase.execute(request) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success(response.data)) - } else { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error(response.error?.message ?: "Failed to find members by date range")) - } - } catch (e: IllegalArgumentException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Invalid date format or date type. Use YYYY-MM-DD format and MEMBERSHIP_START_DATE or MEMBERSHIP_END_DATE")) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to find members by date range: ${e.message}")) - } - } - - /** - * Validate email uniqueness - */ - @GetMapping("/validate/email/{email}") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun validateEmail( - @PathVariable email: String, - @RequestParam(required = false) excludeMemberId: String? - ): ResponseEntity> { - return try { - val excludeId = excludeMemberId?.let { Uuid.parse(it) } - val request = ValidateMemberDataUseCase.ValidateEmailRequest(email, excludeId) - val response = runBlocking { validateMemberDataUseCase.validateEmail(request) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success(response.data)) - } else { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(response.error?.message ?: "Failed to validate email")) - } - } catch (_: IllegalArgumentException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Invalid member ID format")) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to validate email: ${e.message}")) - } - } - - /** - * Validate membership number uniqueness - */ - @GetMapping("/validate/membership-number/{membershipNumber}") - @PreAuthorize("hasAuthority('PERSON_READ')") - fun validateMembershipNumber( - @PathVariable membershipNumber: String, - @RequestParam(required = false) excludeMemberId: String? - ): ResponseEntity> { - return try { - val excludeId = excludeMemberId?.let { uuidFrom(it) } - val request = ValidateMemberDataUseCase.ValidateMembershipNumberRequest(membershipNumber, excludeId) - val response = runBlocking { validateMemberDataUseCase.validateMembershipNumber(request) } - - if (response.success && response.data != null) { - ResponseEntity.ok(ApiResponse.success(response.data)) - } else { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(response.error?.message ?: "Failed to validate membership number")) - } - } catch (_: IllegalArgumentException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Invalid member ID format")) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to validate membership number: ${e.message}")) - } - } - - /** - * Delete member - */ - @DeleteMapping("/{id}") - @PreAuthorize("hasAuthority('PERSON_DELETE')") - fun deleteMember(@PathVariable id: String): ResponseEntity> { - return try { - val memberId = uuidFrom(id) - val request = DeleteMemberUseCase.DeleteMemberRequest(memberId) - val response = runBlocking { deleteMemberUseCase.execute(request) } - - if (response.success) { - ResponseEntity.ok(ApiResponse.success("Member deleted successfully")) - } else { - val statusCode = when (response.error?.code) { - "MEMBER_NOT_FOUND" -> HttpStatus.NOT_FOUND - else -> HttpStatus.BAD_REQUEST - } - ResponseEntity.status(statusCode) - .body(ApiResponse.error(response.error?.message ?: "Failed to delete member")) - } - } catch (_: IllegalArgumentException) { - ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ApiResponse.error("Invalid member ID format")) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("Failed to delete member: ${e.message}")) - } - } - - data class CreateMemberRequest( - val firstName: String, - val lastName: String, - val email: String, - val phone: String? = null, - val dateOfBirth: LocalDate? = null, - val membershipNumber: String, - val membershipStartDate: LocalDate, - val membershipEndDate: LocalDate? = null, - val isActive: Boolean = true, - val address: String? = null, - val emergencyContact: String? = null - ) - - data class UpdateMemberRequest( - val firstName: String, - val lastName: String, - val email: String, - val phone: String? = null, - val dateOfBirth: LocalDate? = null, - val membershipNumber: String, - val membershipStartDate: LocalDate, - val membershipEndDate: LocalDate? = null, - val isActive: Boolean = true, - val address: String? = null, - val emergencyContact: String? = null - ) - - data class MemberStats( - val totalActive: Long, - val totalMembers: Long - ) } diff --git a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt index 5508af90..3105f964 100644 --- a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt +++ b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/events/MemberEvents.kt @@ -1,8 +1,8 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class, kotlin.time.ExperimentalTime::class) package at.mocode.members.domain.events import kotlin.uuid.Uuid -import kotlinx.datetime.Instant +import kotlin.time.Instant import kotlinx.datetime.LocalDate /** diff --git a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt index b59dc84d..528dfa4a 100644 --- a/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt +++ b/services/members/members-domain/src/main/kotlin/at/mocode/members/domain/model/Member.kt @@ -1,15 +1,16 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class, kotlin.time.ExperimentalTime::class) package at.mocode.members.domain.model import at.mocode.core.domain.serialization.KotlinInstantSerializer import at.mocode.core.domain.serialization.KotlinLocalDateSerializer import at.mocode.core.domain.serialization.UuidSerializer import kotlin.uuid.Uuid -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant +import kotlin.time.Clock +import kotlin.time.Instant import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.toKotlinLocalDate +import java.time.ZoneId +import java.time.LocalDate as JLocalDate import kotlinx.serialization.Serializable /** @@ -81,7 +82,7 @@ data class Member( fun isMembershipValid(): Boolean { if (!isActive) return false - val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + val today = JLocalDate.now(ZoneId.systemDefault()).toKotlinLocalDate() return membershipEndDate?.let { endDate -> today <= endDate } ?: true // If no end date, membership is valid indefinitely diff --git a/services/members/members-infrastructure/build.gradle.kts b/services/members/members-infrastructure/build.gradle.kts index 47b3568d..dba96703 100644 --- a/services/members/members-infrastructure/build.gradle.kts +++ b/services/members/members-infrastructure/build.gradle.kts @@ -3,8 +3,8 @@ plugins { // kotlin("plugin.spring") // kotlin("plugin.jpa") version "2.1.21" - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.spring) + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinSpring) // KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block // und alle Spring-Boot-spezifischen Gradle-Tasks frei. @@ -17,8 +17,7 @@ plugins { dependencies { api(platform(projects.platform.platformBom)) - implementation(projects.members.membersDomain) - implementation(projects.members.membersApplication) + implementation(projects.services.members.membersDomain) implementation(projects.core.coreDomain) implementation(projects.core.coreUtils) implementation(projects.infrastructure.cache.cacheApi) diff --git a/settings.gradle.kts b/settings.gradle.kts index b932bdd5..c4ce904b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -72,9 +72,10 @@ include(":docs") // Business modules (temporarily disabled - require multiplatform configuration updates) // Note: We enable only the Members modules needed for API contracts to support the Members client feature. */ -// Members modules are currently disabled to keep the client build lean. -// We consume the Members REST API from the client without compiling backend modules here. -// include(":services:members:members-domain") +// Members modules – enabled to provide the REST API contract used by the client +include(":services:members:members-domain") +// keep application out for now (mismatch with core contracts); expose API directly via repository // include(":services:members:members-application") -// include(":services:members:members-api") +include(":services:members:members-infrastructure") +include(":services:members:members-api") // other business modules remain disabled