fixing web-app
This commit is contained in:
@@ -1,333 +0,0 @@
|
||||
# Members Module
|
||||
|
||||
## Überblick
|
||||
|
||||
Das Members-Modul ist eine umfassende Lösung zur Verwaltung von Mitgliedern für Pferdesportorganisationen. Es implementiert eine saubere Architektur mit Domain-Driven Design und bietet vollständige CRUD-Operationen sowie erweiterte Geschäftslogik für die Mitgliederverwaltung.
|
||||
|
||||
## Funktionalität
|
||||
|
||||
### Verwaltete Entität
|
||||
|
||||
#### Mitglied (Member)
|
||||
- **Persönliche Informationen**: Vor- und Nachname, E-Mail, Telefon, Geburtsdatum
|
||||
- **Mitgliedschaftsinformationen**: Mitgliedsnummer, Start-/Enddatum, Aktivitätsstatus
|
||||
- **Zusätzliche Informationen**: Adresse, Notfallkontakt
|
||||
- **Audit-Felder**: Erstellungs- und Aktualisierungszeitstempel
|
||||
- **Geschäftslogik**: Validierung, Mitgliedschaftsgültigkeit, Vollständiger Name
|
||||
|
||||
### Geschäftsoperationen
|
||||
|
||||
Das Modul bietet 18+ spezialisierte Repository-Operationen:
|
||||
|
||||
#### Basis-CRUD-Operationen
|
||||
- `findById(id)` - Mitglied nach UUID suchen
|
||||
- `save(member)` - Mitglied speichern (erstellen/aktualisieren)
|
||||
- `delete(id)` - Mitglied löschen
|
||||
|
||||
#### Such-Operationen
|
||||
- `findByMembershipNumber(number)` - Nach Mitgliedsnummer suchen
|
||||
- `findByEmail(email)` - Nach E-Mail-Adresse suchen
|
||||
- `findByName(searchTerm, limit)` - Nach Namen suchen (Teilübereinstimmung)
|
||||
- `findAllActive(limit, offset)` - Alle aktiven Mitglieder
|
||||
- `findAll(limit, offset)` - Alle Mitglieder (aktiv und inaktiv)
|
||||
|
||||
#### Datumsbasierte Abfragen
|
||||
- `findByMembershipStartDateRange(start, end)` - Mitglieder nach Startdatum-Bereich
|
||||
- `findByMembershipEndDateRange(start, end)` - Mitglieder nach Enddatum-Bereich
|
||||
- `findMembersWithExpiringMembership(daysAhead)` - Mitglieder mit ablaufender Mitgliedschaft
|
||||
|
||||
#### Validierungs-Operationen
|
||||
- `existsByMembershipNumber(number, excludeId)` - Prüfung auf doppelte Mitgliedsnummer
|
||||
- `existsByEmail(email, excludeId)` - Prüfung auf doppelte E-Mail-Adresse
|
||||
|
||||
#### Zähl-Operationen
|
||||
- `countActive()` - Anzahl aktiver Mitglieder
|
||||
- `countAll()` - Gesamtanzahl aller Mitglieder
|
||||
|
||||
## Architektur
|
||||
|
||||
Das Modul folgt der Clean Architecture mit klarer Trennung der Verantwortlichkeiten:
|
||||
|
||||
```
|
||||
members/
|
||||
├── members-domain/ # Domain Layer
|
||||
│ ├── model/ # Domain Models
|
||||
│ │ └── Member.kt # Mitglied-Entität mit Geschäftslogik
|
||||
│ ├── repository/ # Repository Interfaces
|
||||
│ │ └── MemberRepository.kt # 18+ Geschäftsoperationen
|
||||
│ └── events/ # Domain Events
|
||||
│ └── MemberEvents.kt # Mitgliedschafts-Events
|
||||
├── members-application/ # Application Layer
|
||||
│ └── usecase/ # Use Cases
|
||||
│ └── FindExpiringMembershipsUseCase.kt
|
||||
├── members-infrastructure/ # Infrastructure Layer
|
||||
│ ├── persistence/ # Database Implementation
|
||||
│ │ ├── MemberRepositoryImpl.kt
|
||||
│ │ └── MemberTable.kt
|
||||
│ └── repository/ # Alternative Implementations
|
||||
│ └── InMemoryMemberRepository.kt
|
||||
├── members-api/ # API Layer
|
||||
│ └── rest/ # REST Controllers
|
||||
│ └── MemberController.kt
|
||||
└── members-service/ # Service Layer
|
||||
├── MembersServiceApplication.kt
|
||||
└── test/ # Integration Tests
|
||||
└── MemberServiceIntegrationTest.kt
|
||||
```
|
||||
|
||||
### Domain Layer
|
||||
- **1 Domain Model** mit reichhaltiger Geschäftslogik
|
||||
- **1 Repository Interface** mit 18+ Geschäftsoperationen
|
||||
- **Domain Events** für Mitgliedschaftsänderungen
|
||||
- **Keine Abhängigkeiten** zu anderen Layern
|
||||
|
||||
### Application Layer
|
||||
- **Use Cases** für komplexe Geschäftsoperationen
|
||||
- **Orchestrierung** von Domain-Services
|
||||
- **Anwendungslogik** ohne UI-Abhängigkeiten
|
||||
|
||||
### Infrastructure Layer
|
||||
- **Datenbankzugriff** mit Exposed ORM
|
||||
- **Repository-Implementierung** mit PostgreSQL
|
||||
- **In-Memory-Repository** für Tests
|
||||
- **Datenbankschema** und Migrationen
|
||||
|
||||
### API Layer
|
||||
- **REST-Controller** für HTTP-Endpunkte
|
||||
- **DTO-Mapping** zwischen Domain und API
|
||||
- **Validierung** und Fehlerbehandlung
|
||||
|
||||
### Service Layer
|
||||
- **Spring Boot Anwendung**
|
||||
- **Dependency Injection** Konfiguration
|
||||
- **Integrationstests**
|
||||
|
||||
## Domain Model Details
|
||||
|
||||
### Member-Entität
|
||||
|
||||
```kotlin
|
||||
data class Member(
|
||||
val memberId: Uuid,
|
||||
|
||||
// Persönliche Informationen
|
||||
var firstName: String,
|
||||
var lastName: String,
|
||||
var email: String,
|
||||
var phone: String? = null,
|
||||
var dateOfBirth: LocalDate? = null,
|
||||
|
||||
// Mitgliedschaftsinformationen
|
||||
var membershipNumber: String,
|
||||
var membershipStartDate: LocalDate,
|
||||
var membershipEndDate: LocalDate? = null,
|
||||
var isActive: Boolean = true,
|
||||
|
||||
// Zusätzliche Informationen
|
||||
var address: String? = null,
|
||||
var emergencyContact: String? = null,
|
||||
|
||||
// Audit-Felder
|
||||
val createdAt: Instant,
|
||||
var updatedAt: Instant
|
||||
)
|
||||
```
|
||||
|
||||
### Geschäftslogik-Methoden
|
||||
|
||||
- `getFullName()` - Vollständiger Name des Mitglieds
|
||||
- `isMembershipValid()` - Prüfung der Mitgliedschaftsgültigkeit
|
||||
- `validate()` - Datenvalidierung mit Fehlerliste
|
||||
- `withUpdatedTimestamp()` - Kopie mit aktualisiertem Zeitstempel
|
||||
|
||||
## Repository-Operationen
|
||||
|
||||
### Erweiterte Such-Features
|
||||
|
||||
```kotlin
|
||||
// Mitglieder mit ablaufender Mitgliedschaft finden
|
||||
val expiringMembers = memberRepository.findMembersWithExpiringMembership(30)
|
||||
|
||||
// Mitglieder nach Datumsbereich suchen
|
||||
val newMembers = memberRepository.findByMembershipStartDateRange(
|
||||
startDate = LocalDate(2024, 1, 1),
|
||||
endDate = LocalDate(2024, 12, 31)
|
||||
)
|
||||
|
||||
// Namenssuche mit Teilübereinstimmung
|
||||
val searchResults = memberRepository.findByName("Schmidt", limit = 10)
|
||||
```
|
||||
|
||||
### Validierung und Duplikatsprüfung
|
||||
|
||||
```kotlin
|
||||
// Prüfung auf doppelte Mitgliedsnummer
|
||||
val numberExists = memberRepository.existsByMembershipNumber("M2024001")
|
||||
|
||||
// Prüfung auf doppelte E-Mail (mit Ausschluss für Updates)
|
||||
val emailExists = memberRepository.existsByEmail(
|
||||
email = "max@example.com",
|
||||
excludeMemberId = existingMember.memberId
|
||||
)
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### FindExpiringMembershipsUseCase
|
||||
|
||||
Findet Mitglieder mit ablaufenden Mitgliedschaften und kann automatische Benachrichtigungen auslösen.
|
||||
|
||||
```kotlin
|
||||
class FindExpiringMembershipsUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
suspend fun execute(daysAhead: Int = 30): List<Member> {
|
||||
return memberRepository.findMembersWithExpiringMembership(daysAhead)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
Das Members-Modul stellt REST-Endpunkte über den MemberController bereit:
|
||||
|
||||
- `GET /api/members` - Alle aktiven Mitglieder abrufen
|
||||
- `GET /api/members/{id}` - Mitglied nach ID abrufen
|
||||
- `GET /api/members/search?name={name}` - Mitglieder nach Namen suchen
|
||||
- `GET /api/members/expiring?days={days}` - Mitglieder mit ablaufender Mitgliedschaft
|
||||
- `POST /api/members` - Neues Mitglied erstellen
|
||||
- `PUT /api/members/{id}` - Mitglied aktualisieren
|
||||
- `DELETE /api/members/{id}` - Mitglied löschen
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Datenbankschema
|
||||
|
||||
Das Modul verwendet eine `members`-Tabelle mit folgenden Spalten:
|
||||
- `member_id` (UUID, Primary Key)
|
||||
- `first_name`, `last_name`, `email` (Required)
|
||||
- `phone`, `date_of_birth` (Optional)
|
||||
- `membership_number` (Unique)
|
||||
- `membership_start_date`, `membership_end_date`
|
||||
- `is_active` (Boolean)
|
||||
- `address`, `emergency_contact` (Optional)
|
||||
- `created_at`, `updated_at` (Timestamps)
|
||||
|
||||
### Service-Konfiguration
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
members:
|
||||
service:
|
||||
name: members-service
|
||||
port: 8082
|
||||
database:
|
||||
url: jdbc:postgresql://localhost:5432/meldestelle
|
||||
table: members
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Das Modul enthält umfassende Integrationstests:
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `should find members with expiring membership`() {
|
||||
// Test-Implementierung für ablaufende Mitgliedschaften
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should validate unique membership number`() {
|
||||
// Test für Eindeutigkeit der Mitgliedsnummer
|
||||
}
|
||||
```
|
||||
|
||||
### Test-Datenbank
|
||||
|
||||
Verwendet H2 In-Memory-Datenbank für Tests mit automatischem Schema-Setup.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM openjdk:21-jre-slim
|
||||
COPY members-service.jar app.jar
|
||||
EXPOSE 8082
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: members-service
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: members-service
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: members-service
|
||||
image: meldestelle/members-service:latest
|
||||
ports:
|
||||
- containerPort: 8082
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Metriken
|
||||
|
||||
- Anzahl aktiver Mitglieder
|
||||
- Anzahl ablaufender Mitgliedschaften
|
||||
- API-Response-Zeiten
|
||||
- Datenbankverbindungs-Pool
|
||||
|
||||
### Health Checks
|
||||
|
||||
- Datenbankverbindung
|
||||
- Service-Verfügbarkeit
|
||||
- Speicherverbrauch
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Lokale Entwicklung
|
||||
|
||||
```bash
|
||||
# Service starten
|
||||
./gradlew :members:members-service:bootRun
|
||||
|
||||
# Tests ausführen
|
||||
./gradlew :members:test
|
||||
|
||||
# Integration Tests
|
||||
./gradlew :members:members-service:test
|
||||
```
|
||||
|
||||
### Code-Qualität
|
||||
|
||||
- **Kotlin Coding Standards**
|
||||
- **100% Test Coverage** für Domain Layer
|
||||
- **Integration Tests** für alle Use Cases
|
||||
- **API-Dokumentation** mit OpenAPI
|
||||
|
||||
## Zukünftige Erweiterungen
|
||||
|
||||
1. **Mitgliedschaftstypen** - Verschiedene Mitgliedschaftskategorien
|
||||
2. **Beitragsverwaltung** - Integration mit Zahlungssystem
|
||||
3. **Mitgliedschaftshistorie** - Tracking von Änderungen
|
||||
4. **Bulk-Operationen** - Massenimport/-export
|
||||
5. **Benachrichtigungen** - Automatische E-Mail-Benachrichtigungen
|
||||
6. **Reporting** - Mitgliedschaftsstatistiken und Reports
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung**: 25. Juli 2025
|
||||
|
||||
Für weitere Informationen zur Gesamtarchitektur siehe [README.md](../README.md).
|
||||
@@ -1,29 +0,0 @@
|
||||
plugins {
|
||||
// kotlin("jvm")
|
||||
// kotlin("plugin.spring")
|
||||
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
|
||||
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||
alias(libs.plugins.spring.boot)
|
||||
|
||||
// Dependency Management für konsistente Spring-Versionen
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.members.membersDomain)
|
||||
implementation(projects.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")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
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.repository.MemberRepository
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
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 org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse
|
||||
|
||||
/**
|
||||
* 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<Pair<String?, Any>>) {
|
||||
// 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")
|
||||
class MemberController(
|
||||
@Qualifier("memberRepositoryImpl") private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
// Einfache No-op EventPublisher Implementierung vorerst
|
||||
private val eventPublisher = NoOpEventPublisher()
|
||||
|
||||
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)
|
||||
|
||||
/**
|
||||
* Hilfsmethode zur Behandlung gemeinsamer Antwortmuster für Use-Case-Ausführung
|
||||
*/
|
||||
private inline fun <T> handleUseCaseExecution(
|
||||
crossinline operation: suspend () -> ApiResponse<T>,
|
||||
successStatus: HttpStatus = HttpStatus.OK,
|
||||
crossinline extractData: (T) -> Any = { it as Any }
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
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<Any>(response.error?.message ?: "Operation failed"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error<Any>("Invalid input format: ${e.message}"))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Internal server error: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsmethode zur Behandlung von Repository-Operationen mit gemeinsamer Fehlerbehandlung
|
||||
*/
|
||||
private inline fun <T> handleRepositoryOperation(
|
||||
crossinline operation: () -> T,
|
||||
errorMessage: String = "Operation failed"
|
||||
): ResponseEntity<ApiResponse<T>> {
|
||||
return try {
|
||||
val result = runBlocking { operation() }
|
||||
ResponseEntity.ok(ApiResponse.success(result))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<T>("$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
|
||||
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<ApiResponse<List<*>>> {
|
||||
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))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<List<*>>("Failed to retrieve members: ${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}")
|
||||
fun getMemberById(
|
||||
@Parameter(description = "Member unique identifier", example = "123e4567-e89b-12d3-a456-426614174000")
|
||||
@PathVariable id: String
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
return handleUseCaseExecution(
|
||||
operation = {
|
||||
val memberId = uuidFrom(id)
|
||||
val request = GetMemberUseCase.GetMemberRequest(memberId)
|
||||
getMemberUseCase.execute(request)
|
||||
},
|
||||
extractData = { (it as GetMemberUseCase.GetMemberResponse).member }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get member by membership number
|
||||
*/
|
||||
@GetMapping("/by-membership-number/{membershipNumber}")
|
||||
fun getMemberByMembershipNumber(@PathVariable membershipNumber: String): ResponseEntity<ApiResponse<*>> {
|
||||
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<Any>("Member not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Failed to retrieve member: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get member by email
|
||||
*/
|
||||
@GetMapping("/by-email/{email}")
|
||||
fun getMemberByEmail(@PathVariable email: String): ResponseEntity<ApiResponse<*>> {
|
||||
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<Any>("Member not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Failed to retrieve member: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get member statistics
|
||||
*/
|
||||
@GetMapping("/stats")
|
||||
fun getMemberStats(): ResponseEntity<ApiResponse<MemberStats>> {
|
||||
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<MemberStats>("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
|
||||
fun createMember(
|
||||
@Parameter(description = "Member creation request data")
|
||||
@RequestBody createRequest: CreateMemberRequest
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
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}")
|
||||
fun updateMember(@PathVariable id: String, @RequestBody updateRequest: UpdateMemberRequest): ResponseEntity<ApiResponse<*>> {
|
||||
return try {
|
||||
val memberId = uuidFrom(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<Any>(response.error?.message ?: "Failed to update member"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error<Any>("Invalid member ID format"))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Failed to update member: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get members with expiring memberships
|
||||
*/
|
||||
@GetMapping("/expiring-memberships")
|
||||
fun getExpiringMemberships(
|
||||
@RequestParam(defaultValue = "30") daysAhead: Int
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
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<Any>(response.error?.message ?: "Failed to find expiring memberships"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Failed to find expiring memberships: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get members by date range
|
||||
*/
|
||||
@GetMapping("/by-date-range")
|
||||
fun getMembersByDateRange(
|
||||
@RequestParam startDate: String,
|
||||
@RequestParam endDate: String,
|
||||
@RequestParam(defaultValue = "MEMBERSHIP_START_DATE") dateType: String
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
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<Any>(response.error?.message ?: "Failed to find members by date range"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error<Any>("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<Any>("Failed to find members by date range: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email uniqueness
|
||||
*/
|
||||
@GetMapping("/validate/email/{email}")
|
||||
fun validateEmail(
|
||||
@PathVariable email: String,
|
||||
@RequestParam(required = false) excludeMemberId: String?
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
return try {
|
||||
val excludeId = excludeMemberId?.let { uuidFrom(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<Any>(response.error?.message ?: "Failed to validate email"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error<Any>("Invalid member ID format"))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Failed to validate email: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate membership number uniqueness
|
||||
*/
|
||||
@GetMapping("/validate/membership-number/{membershipNumber}")
|
||||
fun validateMembershipNumber(
|
||||
@PathVariable membershipNumber: String,
|
||||
@RequestParam(required = false) excludeMemberId: String?
|
||||
): ResponseEntity<ApiResponse<*>> {
|
||||
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<Any>(response.error?.message ?: "Failed to validate membership number"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error<Any>("Invalid member ID format"))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<Any>("Failed to validate membership number: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete member
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteMember(@PathVariable id: String): ResponseEntity<ApiResponse<String>> {
|
||||
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<String>(response.error?.message ?: "Failed to delete member"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(ApiResponse.error<String>("Invalid member ID format"))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(ApiResponse.error<String>("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
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.members.membersDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
-239
@@ -1,239 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import at.mocode.members.domain.events.MemberCreatedEvent
|
||||
import at.mocode.infrastructure.messaging.client.EventPublisher
|
||||
import at.mocode.core.domain.model.ValidationResult
|
||||
import at.mocode.core.domain.model.ValidationError
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Use case for creating new members.
|
||||
*
|
||||
* This use case handles the business logic for creating members,
|
||||
* including validation and persistence.
|
||||
*/
|
||||
class CreateMemberUseCase(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val eventPublisher: EventPublisher
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new member.
|
||||
*/
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the created member.
|
||||
*/
|
||||
data class CreateMemberResponse(
|
||||
val member: Member
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the create member use case.
|
||||
*
|
||||
* @param request The request containing member data
|
||||
* @return ApiResponse with the created member or error information
|
||||
*/
|
||||
suspend fun execute(request: CreateMemberRequest): ApiResponse<CreateMemberResponse> {
|
||||
return try {
|
||||
// Validate the request
|
||||
val validationResult = validateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "VALIDATION_ERROR",
|
||||
message = "Invalid input data",
|
||||
details = errors.associate { it.field to it.message }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicate membership number
|
||||
if (memberRepository.existsByMembershipNumber(request.membershipNumber)) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DUPLICATE_MEMBERSHIP_NUMBER",
|
||||
message = "Membership number already exists"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicate email
|
||||
if (memberRepository.existsByEmail(request.email)) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DUPLICATE_EMAIL",
|
||||
message = "Email address already exists"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val member = Member(
|
||||
firstName = request.firstName.trim(),
|
||||
lastName = request.lastName.trim(),
|
||||
email = request.email.trim().lowercase(),
|
||||
phone = request.phone?.trim(),
|
||||
dateOfBirth = request.dateOfBirth,
|
||||
membershipNumber = request.membershipNumber.trim(),
|
||||
membershipStartDate = request.membershipStartDate,
|
||||
membershipEndDate = request.membershipEndDate,
|
||||
isActive = request.isActive,
|
||||
address = request.address?.trim(),
|
||||
emergencyContact = request.emergencyContact?.trim(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Validate the domain object
|
||||
val domainValidationErrors = member.validate()
|
||||
if (domainValidationErrors.isNotEmpty()) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DOMAIN_VALIDATION_ERROR",
|
||||
message = "Domain validation failed",
|
||||
details = domainValidationErrors.mapIndexed { index, error ->
|
||||
"error_$index" to error
|
||||
}.toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Save the member
|
||||
val savedMember = memberRepository.save(member)
|
||||
|
||||
// Publish member created event
|
||||
try {
|
||||
val event = MemberCreatedEvent(
|
||||
eventId = uuid4().toString(),
|
||||
memberId = savedMember.memberId,
|
||||
timestamp = Clock.System.now(),
|
||||
firstName = savedMember.firstName,
|
||||
lastName = savedMember.lastName,
|
||||
email = savedMember.email,
|
||||
membershipNumber = savedMember.membershipNumber,
|
||||
membershipStartDate = savedMember.membershipStartDate,
|
||||
isActive = savedMember.isActive
|
||||
)
|
||||
eventPublisher.publishEvent("member-events", savedMember.memberId.toString(), event)
|
||||
} catch (e: Exception) {
|
||||
// Log the error but don't fail the operation
|
||||
// In a production system, you might want to use a dead letter queue or retry mechanism
|
||||
println("Failed to publish member created event: ${e.message}")
|
||||
}
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = CreateMemberResponse(savedMember)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to create member: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the create member request.
|
||||
*/
|
||||
private fun validateRequest(request: CreateMemberRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Validate first name
|
||||
if (request.firstName.isBlank()) {
|
||||
errors.add(ValidationError("firstName", "First name is required"))
|
||||
} else if (request.firstName.length > 100) {
|
||||
errors.add(ValidationError("firstName", "First name must not exceed 100 characters"))
|
||||
}
|
||||
|
||||
// Validate last name
|
||||
if (request.lastName.isBlank()) {
|
||||
errors.add(ValidationError("lastName", "Last name is required"))
|
||||
} else if (request.lastName.length > 100) {
|
||||
errors.add(ValidationError("lastName", "Last name must not exceed 100 characters"))
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (request.email.isBlank()) {
|
||||
errors.add(ValidationError("email", "Email is required"))
|
||||
} else if (!isValidEmail(request.email)) {
|
||||
errors.add(ValidationError("email", "Email format is invalid"))
|
||||
} else if (request.email.length > 255) {
|
||||
errors.add(ValidationError("email", "Email must not exceed 255 characters"))
|
||||
}
|
||||
|
||||
// Validate membership number
|
||||
if (request.membershipNumber.isBlank()) {
|
||||
errors.add(ValidationError("membershipNumber", "Membership number is required"))
|
||||
} else if (request.membershipNumber.length > 50) {
|
||||
errors.add(ValidationError("membershipNumber", "Membership number must not exceed 50 characters"))
|
||||
}
|
||||
|
||||
// Validate membership dates
|
||||
request.membershipEndDate?.let { endDate ->
|
||||
if (endDate < request.membershipStartDate) {
|
||||
errors.add(ValidationError("membershipEndDate", "Membership end date cannot be before start date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate phone
|
||||
request.phone?.let { phone ->
|
||||
if (phone.length > 50) {
|
||||
errors.add(ValidationError("phone", "Phone number must not exceed 50 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate address
|
||||
request.address?.let { address ->
|
||||
if (address.length > 500) {
|
||||
errors.add(ValidationError("address", "Address must not exceed 500 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate emergency contact
|
||||
request.emergencyContact?.let { contact ->
|
||||
if (contact.length > 255) {
|
||||
errors.add(ValidationError("emergencyContact", "Emergency contact must not exceed 255 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidEmail(email: String): Boolean {
|
||||
return email.contains("@") && email.contains(".") && email.indexOf("@") < email.lastIndexOf(".")
|
||||
}
|
||||
}
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for deleting members.
|
||||
*
|
||||
* This use case handles the business logic for deleting members
|
||||
* from the system.
|
||||
*/
|
||||
class DeleteMemberUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for deleting a member.
|
||||
*/
|
||||
data class DeleteMemberRequest(
|
||||
val memberId: Uuid
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for delete operation.
|
||||
*/
|
||||
data class DeleteMemberResponse(
|
||||
val success: Boolean,
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the delete member use case.
|
||||
*
|
||||
* @param request The request containing member ID to delete
|
||||
* @return ApiResponse with the result or error information
|
||||
*/
|
||||
suspend fun execute(request: DeleteMemberRequest): ApiResponse<DeleteMemberResponse> {
|
||||
return try {
|
||||
// Check if member exists
|
||||
val existingMember = memberRepository.findById(request.memberId)
|
||||
if (existingMember == null) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "MEMBER_NOT_FOUND",
|
||||
message = "Member not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Delete the member
|
||||
val deleted = memberRepository.delete(request.memberId)
|
||||
|
||||
if (deleted) {
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = DeleteMemberResponse(
|
||||
success = true,
|
||||
message = "Member deleted successfully"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DELETE_FAILED",
|
||||
message = "Failed to delete member"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to delete member: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
|
||||
/**
|
||||
* Use case for finding members with expiring memberships.
|
||||
*
|
||||
* This use case handles the business logic for finding members
|
||||
* whose memberships are expiring within a specified number of days.
|
||||
*/
|
||||
class FindExpiringMembershipsUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for finding expiring memberships.
|
||||
*/
|
||||
data class FindExpiringMembershipsRequest(
|
||||
val daysAhead: Int = 30
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the list of members with expiring memberships.
|
||||
*/
|
||||
data class FindExpiringMembershipsResponse(
|
||||
val members: List<Member>,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the find expiring memberships use case.
|
||||
*
|
||||
* @param request The request containing the number of days to look ahead
|
||||
* @return ApiResponse with the list of members or error information
|
||||
*/
|
||||
suspend fun execute(request: FindExpiringMembershipsRequest): ApiResponse<FindExpiringMembershipsResponse> {
|
||||
return try {
|
||||
// Validate input
|
||||
if (request.daysAhead < 0) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INVALID_DAYS_AHEAD",
|
||||
message = "Days ahead must be a positive number"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val members = memberRepository.findMembersWithExpiringMembership(request.daysAhead)
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = FindExpiringMembershipsResponse(
|
||||
members = members,
|
||||
count = members.size
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to find expiring memberships: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Use case for finding members by date ranges.
|
||||
*
|
||||
* This use case handles the business logic for finding members
|
||||
* based on their membership start or end date ranges.
|
||||
*/
|
||||
class FindMembersByDateRangeUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for finding members by date range.
|
||||
*/
|
||||
data class FindMembersByDateRangeRequest(
|
||||
val startDate: LocalDate,
|
||||
val endDate: LocalDate,
|
||||
val dateType: DateRangeType
|
||||
)
|
||||
|
||||
/**
|
||||
* Type of date range to search by.
|
||||
*/
|
||||
enum class DateRangeType {
|
||||
MEMBERSHIP_START_DATE,
|
||||
MEMBERSHIP_END_DATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Response data containing the list of members within the date range.
|
||||
*/
|
||||
data class FindMembersByDateRangeResponse(
|
||||
val members: List<Member>,
|
||||
val count: Int,
|
||||
val dateType: DateRangeType,
|
||||
val startDate: LocalDate,
|
||||
val endDate: LocalDate
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the find members by date range use case.
|
||||
*
|
||||
* @param request The request containing the date range and type
|
||||
* @return ApiResponse with the list of members or error information
|
||||
*/
|
||||
suspend fun execute(request: FindMembersByDateRangeRequest): ApiResponse<FindMembersByDateRangeResponse> {
|
||||
return try {
|
||||
// Validate input
|
||||
if (request.startDate > request.endDate) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INVALID_DATE_RANGE",
|
||||
message = "Start date cannot be after end date"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val members = when (request.dateType) {
|
||||
DateRangeType.MEMBERSHIP_START_DATE ->
|
||||
memberRepository.findByMembershipStartDateRange(request.startDate, request.endDate)
|
||||
DateRangeType.MEMBERSHIP_END_DATE ->
|
||||
memberRepository.findByMembershipEndDateRange(request.startDate, request.endDate)
|
||||
}
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = FindMembersByDateRangeResponse(
|
||||
members = members,
|
||||
count = members.size,
|
||||
dateType = request.dateType,
|
||||
startDate = request.startDate,
|
||||
endDate = request.endDate
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to find members by date range: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving members.
|
||||
*
|
||||
* This use case handles the business logic for retrieving members
|
||||
* by various criteria.
|
||||
*/
|
||||
class GetMemberUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for getting a member by ID.
|
||||
*/
|
||||
data class GetMemberRequest(
|
||||
val memberId: Uuid
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the retrieved member.
|
||||
*/
|
||||
data class GetMemberResponse(
|
||||
val member: Member
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the get member use case.
|
||||
*
|
||||
* @param request The request containing member ID
|
||||
* @return ApiResponse with the member or error information
|
||||
*/
|
||||
suspend fun execute(request: GetMemberRequest): ApiResponse<GetMemberResponse> {
|
||||
return try {
|
||||
val member = memberRepository.findById(request.memberId)
|
||||
|
||||
if (member != null) {
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = GetMemberResponse(member)
|
||||
)
|
||||
} else {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "MEMBER_NOT_FOUND",
|
||||
message = "Member not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to retrieve member: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member by membership number.
|
||||
*/
|
||||
suspend fun getByMembershipNumber(membershipNumber: String): ApiResponse<GetMemberResponse> {
|
||||
return try {
|
||||
val member = memberRepository.findByMembershipNumber(membershipNumber)
|
||||
|
||||
if (member != null) {
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = GetMemberResponse(member)
|
||||
)
|
||||
} else {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "MEMBER_NOT_FOUND",
|
||||
message = "Member not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to retrieve member: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a member by email address.
|
||||
*/
|
||||
suspend fun getByEmail(email: String): ApiResponse<GetMemberResponse> {
|
||||
return try {
|
||||
val member = memberRepository.findByEmail(email)
|
||||
|
||||
if (member != null) {
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = GetMemberResponse(member)
|
||||
)
|
||||
} else {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "MEMBER_NOT_FOUND",
|
||||
message = "Member not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to retrieve member: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import at.mocode.core.domain.model.ValidationResult
|
||||
import at.mocode.core.domain.model.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Use case for updating existing members.
|
||||
*
|
||||
* This use case handles the business logic for updating members,
|
||||
* including validation and persistence.
|
||||
*/
|
||||
class UpdateMemberUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for updating a member.
|
||||
*/
|
||||
data class UpdateMemberRequest(
|
||||
val memberId: Uuid,
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the updated member.
|
||||
*/
|
||||
data class UpdateMemberResponse(
|
||||
val member: Member
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the update member use case.
|
||||
*
|
||||
* @param request The request containing updated member data
|
||||
* @return ApiResponse with the updated member or error information
|
||||
*/
|
||||
suspend fun execute(request: UpdateMemberRequest): ApiResponse<UpdateMemberResponse> {
|
||||
return try {
|
||||
// Check if member exists
|
||||
val existingMember = memberRepository.findById(request.memberId)
|
||||
if (existingMember == null) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "MEMBER_NOT_FOUND",
|
||||
message = "Member not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "VALIDATION_ERROR",
|
||||
message = "Invalid input data",
|
||||
details = errors.associate { it.field to it.message }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicate membership number (excluding current member)
|
||||
if (memberRepository.existsByMembershipNumber(request.membershipNumber, request.memberId)) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DUPLICATE_MEMBERSHIP_NUMBER",
|
||||
message = "Membership number already exists"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicate email (excluding current member)
|
||||
if (memberRepository.existsByEmail(request.email, request.memberId)) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DUPLICATE_EMAIL",
|
||||
message = "Email address already exists"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Update the member
|
||||
val updatedMember = existingMember.copy(
|
||||
firstName = request.firstName.trim(),
|
||||
lastName = request.lastName.trim(),
|
||||
email = request.email.trim().lowercase(),
|
||||
phone = request.phone?.trim(),
|
||||
dateOfBirth = request.dateOfBirth,
|
||||
membershipNumber = request.membershipNumber.trim(),
|
||||
membershipStartDate = request.membershipStartDate,
|
||||
membershipEndDate = request.membershipEndDate,
|
||||
isActive = request.isActive,
|
||||
address = request.address?.trim(),
|
||||
emergencyContact = request.emergencyContact?.trim()
|
||||
).withUpdatedTimestamp()
|
||||
|
||||
// Validate the domain object
|
||||
val domainValidationErrors = updatedMember.validate()
|
||||
if (domainValidationErrors.isNotEmpty()) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DOMAIN_VALIDATION_ERROR",
|
||||
message = "Domain validation failed",
|
||||
details = domainValidationErrors.mapIndexed { index, error ->
|
||||
"error_$index" to error
|
||||
}.toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Save the updated member
|
||||
val savedMember = memberRepository.save(updatedMember)
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = UpdateMemberResponse(savedMember)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to update member: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the update member request.
|
||||
*/
|
||||
private fun validateRequest(request: UpdateMemberRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Validate first name
|
||||
if (request.firstName.isBlank()) {
|
||||
errors.add(ValidationError("firstName", "First name is required"))
|
||||
} else if (request.firstName.length > 100) {
|
||||
errors.add(ValidationError("firstName", "First name must not exceed 100 characters"))
|
||||
}
|
||||
|
||||
// Validate last name
|
||||
if (request.lastName.isBlank()) {
|
||||
errors.add(ValidationError("lastName", "Last name is required"))
|
||||
} else if (request.lastName.length > 100) {
|
||||
errors.add(ValidationError("lastName", "Last name must not exceed 100 characters"))
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (request.email.isBlank()) {
|
||||
errors.add(ValidationError("email", "Email is required"))
|
||||
} else if (!isValidEmail(request.email)) {
|
||||
errors.add(ValidationError("email", "Email format is invalid"))
|
||||
} else if (request.email.length > 255) {
|
||||
errors.add(ValidationError("email", "Email must not exceed 255 characters"))
|
||||
}
|
||||
|
||||
// Validate membership number
|
||||
if (request.membershipNumber.isBlank()) {
|
||||
errors.add(ValidationError("membershipNumber", "Membership number is required"))
|
||||
} else if (request.membershipNumber.length > 50) {
|
||||
errors.add(ValidationError("membershipNumber", "Membership number must not exceed 50 characters"))
|
||||
}
|
||||
|
||||
// Validate membership dates
|
||||
request.membershipEndDate?.let { endDate ->
|
||||
if (endDate < request.membershipStartDate) {
|
||||
errors.add(ValidationError("membershipEndDate", "Membership end date cannot be before start date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate phone
|
||||
request.phone?.let { phone ->
|
||||
if (phone.length > 50) {
|
||||
errors.add(ValidationError("phone", "Phone number must not exceed 50 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate address
|
||||
request.address?.let { address ->
|
||||
if (address.length > 500) {
|
||||
errors.add(ValidationError("address", "Address must not exceed 500 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate emergency contact
|
||||
request.emergencyContact?.let { contact ->
|
||||
if (contact.length > 255) {
|
||||
errors.add(ValidationError("emergencyContact", "Emergency contact must not exceed 255 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidEmail(email: String): Boolean {
|
||||
return email.contains("@") && email.contains(".") && email.indexOf("@") < email.lastIndexOf(".")
|
||||
}
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for validating member data.
|
||||
*
|
||||
* This use case handles the business logic for validating
|
||||
* member data such as email and membership number uniqueness.
|
||||
*/
|
||||
class ValidateMemberDataUseCase(
|
||||
private val memberRepository: MemberRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for validating email uniqueness.
|
||||
*/
|
||||
data class ValidateEmailRequest(
|
||||
val email: String,
|
||||
val excludeMemberId: Uuid? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request data for validating membership number uniqueness.
|
||||
*/
|
||||
data class ValidateMembershipNumberRequest(
|
||||
val membershipNumber: String,
|
||||
val excludeMemberId: Uuid? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for validation results.
|
||||
*/
|
||||
data class ValidationResponse(
|
||||
val isValid: Boolean,
|
||||
val exists: Boolean,
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Validates if an email address is unique.
|
||||
*
|
||||
* @param request The request containing email and optional member ID to exclude
|
||||
* @return ApiResponse with validation result
|
||||
*/
|
||||
suspend fun validateEmail(request: ValidateEmailRequest): ApiResponse<ValidationResponse> {
|
||||
return try {
|
||||
// Basic email format validation
|
||||
if (request.email.isBlank()) {
|
||||
return ApiResponse(
|
||||
success = true,
|
||||
data = ValidationResponse(
|
||||
isValid = false,
|
||||
exists = false,
|
||||
message = "Email is required"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!isValidEmailFormat(request.email)) {
|
||||
return ApiResponse(
|
||||
success = true,
|
||||
data = ValidationResponse(
|
||||
isValid = false,
|
||||
exists = false,
|
||||
message = "Email format is invalid"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val exists = memberRepository.existsByEmail(request.email, request.excludeMemberId)
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = ValidationResponse(
|
||||
isValid = !exists,
|
||||
exists = exists,
|
||||
message = if (exists) "Email already exists" else "Email is available"
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to validate email: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a membership number is unique.
|
||||
*
|
||||
* @param request The request containing membership number and optional member ID to exclude
|
||||
* @return ApiResponse with validation result
|
||||
*/
|
||||
suspend fun validateMembershipNumber(request: ValidateMembershipNumberRequest): ApiResponse<ValidationResponse> {
|
||||
return try {
|
||||
// Basic membership number validation
|
||||
if (request.membershipNumber.isBlank()) {
|
||||
return ApiResponse(
|
||||
success = true,
|
||||
data = ValidationResponse(
|
||||
isValid = false,
|
||||
exists = false,
|
||||
message = "Membership number is required"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val exists = memberRepository.existsByMembershipNumber(request.membershipNumber, request.excludeMemberId)
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = ValidationResponse(
|
||||
isValid = !exists,
|
||||
exists = exists,
|
||||
message = if (exists) "Membership number already exists" else "Membership number is available"
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to validate membership number: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic email format validation.
|
||||
*/
|
||||
private fun isValidEmailFormat(email: String): Boolean {
|
||||
return email.contains("@") &&
|
||||
email.contains(".") &&
|
||||
email.indexOf("@") > 0 &&
|
||||
email.lastIndexOf(".") > email.indexOf("@") &&
|
||||
email.length > 5
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
package at.mocode.members.domain.events
|
||||
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Base interface for all member domain events.
|
||||
*/
|
||||
sealed interface MemberEvent {
|
||||
val eventId: String
|
||||
val memberId: Uuid
|
||||
val timestamp: Instant
|
||||
val eventType: String
|
||||
}
|
||||
|
||||
/**
|
||||
* Event published when a new member is created.
|
||||
*/
|
||||
data class MemberCreatedEvent(
|
||||
override val eventId: String,
|
||||
override val memberId: Uuid,
|
||||
override val timestamp: Instant,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String,
|
||||
val membershipNumber: String,
|
||||
val membershipStartDate: LocalDate,
|
||||
val isActive: Boolean
|
||||
) : MemberEvent {
|
||||
override val eventType: String = "MemberCreated"
|
||||
}
|
||||
|
||||
/**
|
||||
* Event published when a member is updated.
|
||||
*/
|
||||
data class MemberUpdatedEvent(
|
||||
override val eventId: String,
|
||||
override val memberId: Uuid,
|
||||
override val timestamp: Instant,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String,
|
||||
val membershipNumber: String,
|
||||
val membershipStartDate: LocalDate,
|
||||
val membershipEndDate: LocalDate?,
|
||||
val isActive: Boolean,
|
||||
val changes: Map<String, Any?>
|
||||
) : MemberEvent {
|
||||
override val eventType: String = "MemberUpdated"
|
||||
}
|
||||
|
||||
/**
|
||||
* Event published when a member is deleted.
|
||||
*/
|
||||
data class MemberDeletedEvent(
|
||||
override val eventId: String,
|
||||
override val memberId: Uuid,
|
||||
override val timestamp: Instant,
|
||||
val membershipNumber: String,
|
||||
val firstName: String,
|
||||
val lastName: String
|
||||
) : MemberEvent {
|
||||
override val eventType: String = "MemberDeleted"
|
||||
}
|
||||
|
||||
/**
|
||||
* Event published when a member's membership is about to expire.
|
||||
*/
|
||||
data class MembershipExpiringEvent(
|
||||
override val eventId: String,
|
||||
override val memberId: Uuid,
|
||||
override val timestamp: Instant,
|
||||
val membershipNumber: String,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val email: String,
|
||||
val membershipEndDate: LocalDate,
|
||||
val daysUntilExpiry: Int
|
||||
) : MemberEvent {
|
||||
override val eventType: String = "MembershipExpiring"
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
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 com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Domain model representing a member in the member management system.
|
||||
*
|
||||
* This entity represents a member of the organization with their personal
|
||||
* information and membership details.
|
||||
*
|
||||
* @property memberId Unique internal identifier for this member (UUID).
|
||||
* @property firstName First name of the member.
|
||||
* @property lastName Last name of the member.
|
||||
* @property email Email address of the member.
|
||||
* @property phone Phone number of the member (optional).
|
||||
* @property dateOfBirth Date of birth of the member (optional).
|
||||
* @property membershipNumber Unique membership number.
|
||||
* @property membershipStartDate Date when membership started.
|
||||
* @property membershipEndDate Date when membership ends (optional).
|
||||
* @property isActive Whether the membership is currently active.
|
||||
* @property address Address of the member (optional).
|
||||
* @property emergencyContact Emergency contact information (optional).
|
||||
* @property createdAt Timestamp when this record was created.
|
||||
* @property updatedAt Timestamp when this record was last updated.
|
||||
*/
|
||||
@Serializable
|
||||
data class Member(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val memberId: Uuid = uuid4(),
|
||||
|
||||
// Personal Information
|
||||
var firstName: String,
|
||||
var lastName: String,
|
||||
var email: String,
|
||||
var phone: String? = null,
|
||||
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
var dateOfBirth: LocalDate? = null,
|
||||
|
||||
// Membership Information
|
||||
var membershipNumber: String,
|
||||
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
var membershipStartDate: LocalDate,
|
||||
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
var membershipEndDate: LocalDate? = null,
|
||||
|
||||
var isActive: Boolean = true,
|
||||
|
||||
// Additional Information
|
||||
var address: String? = null,
|
||||
var emergencyContact: String? = null,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Returns the full name of the member.
|
||||
*/
|
||||
fun getFullName(): String {
|
||||
return "$firstName $lastName"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the membership is currently valid.
|
||||
*/
|
||||
fun isMembershipValid(): Boolean {
|
||||
if (!isActive) return false
|
||||
|
||||
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||
return membershipEndDate?.let { endDate ->
|
||||
today <= endDate
|
||||
} ?: true // If no end date, membership is valid indefinitely
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the member data is consistent.
|
||||
*/
|
||||
fun validate(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
if (firstName.isBlank()) {
|
||||
errors.add("First name is required")
|
||||
}
|
||||
|
||||
if (lastName.isBlank()) {
|
||||
errors.add("Last name is required")
|
||||
}
|
||||
|
||||
if (email.isBlank()) {
|
||||
errors.add("Email is required")
|
||||
} else if (!isValidEmail(email)) {
|
||||
errors.add("Email format is invalid")
|
||||
}
|
||||
|
||||
if (membershipNumber.isBlank()) {
|
||||
errors.add("Membership number is required")
|
||||
}
|
||||
|
||||
membershipEndDate?.let { endDate ->
|
||||
if (endDate < membershipStartDate) {
|
||||
errors.add("Membership end date cannot be before start date")
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this member with updated timestamp.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): Member {
|
||||
return this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
|
||||
private fun isValidEmail(email: String): Boolean {
|
||||
return email.contains("@") && email.contains(".")
|
||||
}
|
||||
}
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
package at.mocode.members.domain.repository
|
||||
|
||||
import at.mocode.members.domain.model.Member
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Repository interface for Member entities.
|
||||
*
|
||||
* This interface defines the contract for data access operations
|
||||
* related to members in the member management bounded context.
|
||||
*/
|
||||
interface MemberRepository {
|
||||
|
||||
/**
|
||||
* Finds a member by their unique identifier.
|
||||
*
|
||||
* @param id The unique identifier of the member
|
||||
* @return The member if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): Member?
|
||||
|
||||
/**
|
||||
* Finds a member by their membership number.
|
||||
*
|
||||
* @param membershipNumber The membership number to search for
|
||||
* @return The member if found, null otherwise
|
||||
*/
|
||||
suspend fun findByMembershipNumber(membershipNumber: String): Member?
|
||||
|
||||
/**
|
||||
* Finds a member by their email address.
|
||||
*
|
||||
* @param email The email address to search for
|
||||
* @return The member if found, null otherwise
|
||||
*/
|
||||
suspend fun findByEmail(email: String): Member?
|
||||
|
||||
/**
|
||||
* Finds members by name (partial match on first or last name).
|
||||
*
|
||||
* @param searchTerm The search term to match against member names
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching members
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<Member>
|
||||
|
||||
/**
|
||||
* Finds all active members.
|
||||
*
|
||||
* @param limit Maximum number of results to return
|
||||
* @param offset Number of results to skip
|
||||
* @return List of active members
|
||||
*/
|
||||
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<Member>
|
||||
|
||||
/**
|
||||
* Finds all members (active and inactive).
|
||||
*
|
||||
* @param limit Maximum number of results to return
|
||||
* @param offset Number of results to skip
|
||||
* @return List of all members
|
||||
*/
|
||||
suspend fun findAll(limit: Int = 100, offset: Int = 0): List<Member>
|
||||
|
||||
/**
|
||||
* Finds members whose membership started within a date range.
|
||||
*
|
||||
* @param startDate The earliest membership start date to include
|
||||
* @param endDate The latest membership start date to include
|
||||
* @return List of members within the specified date range
|
||||
*/
|
||||
suspend fun findByMembershipStartDateRange(startDate: LocalDate, endDate: LocalDate): List<Member>
|
||||
|
||||
/**
|
||||
* Finds members whose membership expires within a date range.
|
||||
*
|
||||
* @param startDate The earliest membership end date to include
|
||||
* @param endDate The latest membership end date to include
|
||||
* @return List of members whose membership expires within the specified date range
|
||||
*/
|
||||
suspend fun findByMembershipEndDateRange(startDate: LocalDate, endDate: LocalDate): List<Member>
|
||||
|
||||
/**
|
||||
* Finds members with expiring memberships (within the next specified days).
|
||||
*
|
||||
* @param daysAhead Number of days to look ahead for expiring memberships
|
||||
* @return List of members with expiring memberships
|
||||
*/
|
||||
suspend fun findMembersWithExpiringMembership(daysAhead: Int = 30): List<Member>
|
||||
|
||||
/**
|
||||
* Saves a member (insert or update).
|
||||
*
|
||||
* @param member The member to save
|
||||
* @return The saved member
|
||||
*/
|
||||
suspend fun save(member: Member): Member
|
||||
|
||||
/**
|
||||
* Deletes a member by their ID.
|
||||
*
|
||||
* @param id The unique identifier of the member to delete
|
||||
* @return True if the member was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Counts the number of active members.
|
||||
*
|
||||
* @return The number of active members
|
||||
*/
|
||||
suspend fun countActive(): Long
|
||||
|
||||
/**
|
||||
* Counts the total number of members.
|
||||
*
|
||||
* @return The total number of members
|
||||
*/
|
||||
suspend fun countAll(): Long
|
||||
|
||||
/**
|
||||
* Checks if a membership number already exists.
|
||||
*
|
||||
* @param membershipNumber The membership number to check
|
||||
* @param excludeMemberId Optional member ID to exclude from the check (for updates)
|
||||
* @return True if the membership number exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByMembershipNumber(membershipNumber: String, excludeMemberId: Uuid? = null): Boolean
|
||||
|
||||
/**
|
||||
* Checks if an email address already exists.
|
||||
*
|
||||
* @param email The email address to check
|
||||
* @param excludeMemberId Optional member ID to exclude from the check (for updates)
|
||||
* @return True if the email exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByEmail(email: String, excludeMemberId: Uuid? = null): Boolean
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
plugins {
|
||||
// kotlin("jvm")
|
||||
// kotlin("plugin.spring")
|
||||
// kotlin("plugin.jpa") version "2.1.21"
|
||||
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
|
||||
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||
alias(libs.plugins.spring.boot)
|
||||
|
||||
// Dependency Management für konsistente Spring-Versionen
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform(projects.platform.platformBom))
|
||||
|
||||
implementation(projects.members.membersDomain)
|
||||
implementation(projects.members.membersApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.cache.cacheApi)
|
||||
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.postgresql:postgresql")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
package at.mocode.members.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
/**
|
||||
* Database implementation of MemberRepository using Exposed ORM.
|
||||
*/
|
||||
@Repository
|
||||
class MemberRepositoryImpl : MemberRepository {
|
||||
|
||||
override suspend fun findById(id: Uuid): Member? = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where { MemberTable.id eq id }
|
||||
.map { rowToMember(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByMembershipNumber(membershipNumber: String): Member? = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where { MemberTable.membershipNumber eq membershipNumber }
|
||||
.map { rowToMember(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): Member? = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where { MemberTable.email.lowerCase() eq email.lowercase() }
|
||||
.map { rowToMember(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Member> = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where {
|
||||
(MemberTable.firstName.lowerCase() like "%${searchTerm.lowercase()}%") or
|
||||
(MemberTable.lastName.lowerCase() like "%${searchTerm.lowercase()}%")
|
||||
}
|
||||
.limit(limit)
|
||||
.map { rowToMember(it) }
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Member> = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where { MemberTable.isActive eq true }
|
||||
.limit(limit, offset.toLong())
|
||||
.map { rowToMember(it) }
|
||||
}
|
||||
|
||||
override suspend fun findAll(limit: Int, offset: Int): List<Member> = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll()
|
||||
.limit(limit, offset.toLong())
|
||||
.map { rowToMember(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByMembershipStartDateRange(startDate: LocalDate, endDate: LocalDate): List<Member> = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where {
|
||||
(MemberTable.membershipStartDate greaterEq startDate) and
|
||||
(MemberTable.membershipStartDate lessEq endDate)
|
||||
}
|
||||
.map { rowToMember(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByMembershipEndDateRange(startDate: LocalDate, endDate: LocalDate): List<Member> = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where {
|
||||
(MemberTable.membershipEndDate.isNotNull()) and
|
||||
(MemberTable.membershipEndDate greaterEq startDate) and
|
||||
(MemberTable.membershipEndDate lessEq endDate)
|
||||
}
|
||||
.map { rowToMember(it) }
|
||||
}
|
||||
|
||||
override suspend fun findMembersWithExpiringMembership(daysAhead: Int): List<Member> = DatabaseFactory.dbQuery {
|
||||
val currentDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||
val futureDate = LocalDate(currentDate.year, currentDate.month, currentDate.dayOfMonth + daysAhead)
|
||||
MemberTable.selectAll().where {
|
||||
(MemberTable.membershipEndDate.isNotNull()) and
|
||||
(MemberTable.membershipEndDate lessEq futureDate) and
|
||||
(MemberTable.isActive eq true)
|
||||
}
|
||||
.map { rowToMember(it) }
|
||||
}
|
||||
|
||||
override suspend fun save(member: Member): Member = DatabaseFactory.dbQuery {
|
||||
val existingMember = MemberTable.selectAll().where { MemberTable.id eq member.memberId }.singleOrNull()
|
||||
|
||||
if (existingMember != null) {
|
||||
// Update existing member
|
||||
MemberTable.update({ MemberTable.id eq member.memberId }) {
|
||||
it[firstName] = member.firstName
|
||||
it[lastName] = member.lastName
|
||||
it[email] = member.email
|
||||
it[phone] = member.phone
|
||||
it[dateOfBirth] = member.dateOfBirth
|
||||
it[membershipNumber] = member.membershipNumber
|
||||
it[membershipStartDate] = member.membershipStartDate
|
||||
it[membershipEndDate] = member.membershipEndDate
|
||||
it[isActive] = member.isActive
|
||||
it[address] = member.address
|
||||
it[emergencyContact] = member.emergencyContact
|
||||
it[updatedAt] = Clock.System.now()
|
||||
}
|
||||
} else {
|
||||
// Insert new member
|
||||
MemberTable.insert {
|
||||
it[id] = member.memberId
|
||||
it[firstName] = member.firstName
|
||||
it[lastName] = member.lastName
|
||||
it[email] = member.email
|
||||
it[phone] = member.phone
|
||||
it[dateOfBirth] = member.dateOfBirth
|
||||
it[membershipNumber] = member.membershipNumber
|
||||
it[membershipStartDate] = member.membershipStartDate
|
||||
it[membershipEndDate] = member.membershipEndDate
|
||||
it[isActive] = member.isActive
|
||||
it[address] = member.address
|
||||
it[emergencyContact] = member.emergencyContact
|
||||
}
|
||||
}
|
||||
member
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
MemberTable.deleteWhere { MemberTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().where { MemberTable.isActive eq true }.count()
|
||||
}
|
||||
|
||||
override suspend fun countAll(): Long = DatabaseFactory.dbQuery {
|
||||
MemberTable.selectAll().count()
|
||||
}
|
||||
|
||||
override suspend fun existsByMembershipNumber(membershipNumber: String, excludeMemberId: Uuid?): Boolean = DatabaseFactory.dbQuery {
|
||||
val query = if (excludeMemberId != null) {
|
||||
MemberTable.selectAll().where {
|
||||
(MemberTable.membershipNumber eq membershipNumber) and
|
||||
(MemberTable.id neq excludeMemberId)
|
||||
}
|
||||
} else {
|
||||
MemberTable.selectAll().where { MemberTable.membershipNumber eq membershipNumber }
|
||||
}
|
||||
query.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByEmail(email: String, excludeMemberId: Uuid?): Boolean = DatabaseFactory.dbQuery {
|
||||
val query = if (excludeMemberId != null) {
|
||||
MemberTable.selectAll().where {
|
||||
(MemberTable.email.lowerCase() eq email.lowercase()) and
|
||||
(MemberTable.id neq excludeMemberId)
|
||||
}
|
||||
} else {
|
||||
MemberTable.selectAll().where { MemberTable.email.lowerCase() eq email.lowercase() }
|
||||
}
|
||||
query.count() > 0
|
||||
}
|
||||
|
||||
private fun rowToMember(row: ResultRow): Member {
|
||||
return Member(
|
||||
memberId = row[MemberTable.id],
|
||||
firstName = row[MemberTable.firstName],
|
||||
lastName = row[MemberTable.lastName],
|
||||
email = row[MemberTable.email],
|
||||
phone = row[MemberTable.phone],
|
||||
dateOfBirth = row[MemberTable.dateOfBirth],
|
||||
membershipNumber = row[MemberTable.membershipNumber],
|
||||
membershipStartDate = row[MemberTable.membershipStartDate],
|
||||
membershipEndDate = row[MemberTable.membershipEndDate],
|
||||
isActive = row[MemberTable.isActive],
|
||||
address = row[MemberTable.address],
|
||||
emergencyContact = row[MemberTable.emergencyContact]
|
||||
)
|
||||
}
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package at.mocode.members.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentTimestamp
|
||||
|
||||
/**
|
||||
* Database table definition for members in the member management context.
|
||||
*
|
||||
* This table stores member information including personal details,
|
||||
* membership information, and contact details.
|
||||
*/
|
||||
object MemberTable : Table("members") {
|
||||
val id = uuid("id").autoGenerate()
|
||||
val firstName = varchar("first_name", 100)
|
||||
val lastName = varchar("last_name", 100)
|
||||
val email = varchar("email", 255).uniqueIndex()
|
||||
val phone = varchar("phone", 50).nullable()
|
||||
val dateOfBirth = date("date_of_birth").nullable()
|
||||
val membershipNumber = varchar("membership_number", 50).uniqueIndex()
|
||||
val membershipStartDate = date("membership_start_date")
|
||||
val membershipEndDate = date("membership_end_date").nullable()
|
||||
val isActive = bool("is_active").default(true)
|
||||
val address = varchar("address", 500).nullable()
|
||||
val emergencyContact = varchar("emergency_contact", 255).nullable()
|
||||
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
package at.mocode.members.infrastructure.repository
|
||||
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* In-memory implementation of MemberRepository for development and testing purposes.
|
||||
*/
|
||||
@Repository
|
||||
class InMemoryMemberRepository : MemberRepository {
|
||||
|
||||
private val members = ConcurrentHashMap<Uuid, Member>()
|
||||
|
||||
override suspend fun findById(id: Uuid): Member? {
|
||||
return members[id]
|
||||
}
|
||||
|
||||
override suspend fun findByMembershipNumber(membershipNumber: String): Member? {
|
||||
return members.values.find { it.membershipNumber == membershipNumber }
|
||||
}
|
||||
|
||||
override suspend fun findByEmail(email: String): Member? {
|
||||
return members.values.find { it.email.equals(email, ignoreCase = true) }
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Member> {
|
||||
return members.values
|
||||
.filter {
|
||||
it.firstName.contains(searchTerm, ignoreCase = true) ||
|
||||
it.lastName.contains(searchTerm, ignoreCase = true)
|
||||
}
|
||||
.take(limit)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Member> {
|
||||
return members.values
|
||||
.filter { it.isActive }
|
||||
.drop(offset)
|
||||
.take(limit)
|
||||
}
|
||||
|
||||
override suspend fun findAll(limit: Int, offset: Int): List<Member> {
|
||||
return members.values
|
||||
.drop(offset)
|
||||
.take(limit)
|
||||
}
|
||||
|
||||
override suspend fun findByMembershipStartDateRange(startDate: LocalDate, endDate: LocalDate): List<Member> {
|
||||
return members.values
|
||||
.filter { it.membershipStartDate >= startDate && it.membershipStartDate <= endDate }
|
||||
}
|
||||
|
||||
override suspend fun findByMembershipEndDateRange(startDate: LocalDate, endDate: LocalDate): List<Member> {
|
||||
return members.values
|
||||
.filter { member ->
|
||||
member.membershipEndDate?.let { memberEndDate ->
|
||||
memberEndDate >= startDate && memberEndDate <= endDate
|
||||
} ?: false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findMembersWithExpiringMembership(daysAhead: Int): List<Member> {
|
||||
// Simplified implementation - returns members with end dates set
|
||||
return members.values
|
||||
.filter { it.membershipEndDate != null }
|
||||
}
|
||||
|
||||
override suspend fun save(member: Member): Member {
|
||||
members[member.memberId] = member
|
||||
return member
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean {
|
||||
return members.remove(id) != null
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long {
|
||||
return members.values.count { it.isActive }.toLong()
|
||||
}
|
||||
|
||||
override suspend fun countAll(): Long {
|
||||
return members.size.toLong()
|
||||
}
|
||||
|
||||
override suspend fun existsByMembershipNumber(membershipNumber: String, excludeMemberId: Uuid?): Boolean {
|
||||
return members.values.any {
|
||||
it.membershipNumber == membershipNumber && it.memberId != excludeMemberId
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun existsByEmail(email: String, excludeMemberId: Uuid?): Boolean {
|
||||
return members.values.any {
|
||||
it.email.equals(email, ignoreCase = true) && it.memberId != excludeMemberId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
plugins {
|
||||
// kotlin("jvm")
|
||||
// kotlin("plugin.spring")
|
||||
// id("org.springframework.boot")
|
||||
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
|
||||
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||
alias(libs.plugins.spring.boot)
|
||||
|
||||
// Dependency Management für konsistente Spring-Versionen
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.members.service.MembersServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.members.membersDomain)
|
||||
implementation(projects.members.membersApplication)
|
||||
implementation(projects.members.membersInfrastructure)
|
||||
implementation(projects.members.membersApi)
|
||||
|
||||
implementation(projects.infrastructure.auth.authClient)
|
||||
implementation(projects.infrastructure.cache.redisCache)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui")
|
||||
|
||||
// Database dependencies
|
||||
implementation("org.jetbrains.exposed:exposed-core")
|
||||
implementation("org.jetbrains.exposed:exposed-dao")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc")
|
||||
implementation("org.jetbrains.exposed:exposed-kotlin-datetime")
|
||||
implementation("com.zaxxer:HikariCP")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
testRuntimeOnly("com.h2database:h2")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.logback.classic) // SLF4J provider for tests
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package at.mocode.members.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
|
||||
/**
|
||||
* Main application class for the Members Service.
|
||||
*
|
||||
* This service provides APIs for managing members and their data.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ComponentScan(basePackages = ["at.mocode.members"])
|
||||
class MembersServiceApplication
|
||||
|
||||
/**
|
||||
* Main entry point for the Members Service application.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<MembersServiceApplication>(*args)
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package at.mocode.members.service.config
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseConfig
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.members.infrastructure.persistence.MemberTable
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
/**
|
||||
* Database configuration for the Members Service.
|
||||
*
|
||||
* This configuration ensures that Database.connect() is called properly
|
||||
* before any Exposed operations are performed.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("!test")
|
||||
class MembersDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(MembersDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initializing database schema for Members Service...")
|
||||
|
||||
try {
|
||||
// Database connection is already initialized by the gateway
|
||||
// Only initialize the schema for this service
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(MemberTable)
|
||||
log.info("Members database schema initialized successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize database schema", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun closeDatabase() {
|
||||
log.info("Closing database connection for Members Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing database connection", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-specific database configuration.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
class MembersTestDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(MembersTestDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeTestDatabase() {
|
||||
log.info("Initializing test database connection for Members Service...")
|
||||
|
||||
try {
|
||||
// Use H2 in-memory database for tests
|
||||
val testConfig = DatabaseConfig(
|
||||
jdbcUrl = "jdbc:h2:mem:members_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
username = "sa",
|
||||
password = "",
|
||||
driverClassName = "org.h2.Driver",
|
||||
maxPoolSize = 5,
|
||||
minPoolSize = 1,
|
||||
autoMigrate = true
|
||||
)
|
||||
|
||||
DatabaseFactory.init(testConfig)
|
||||
log.info("Test database connection initialized successfully")
|
||||
|
||||
// Initialize database schema for tests
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(MemberTable)
|
||||
log.info("Test members database schema initialized successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize test database connection", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun closeTestDatabase() {
|
||||
log.info("Closing test database connection for Members Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Test database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing test database connection", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
package at.mocode.members.service.integration
|
||||
|
||||
import at.mocode.infrastructure.messaging.client.EventPublisher
|
||||
import at.mocode.members.api.rest.MemberController
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Integration tests for the Members Service.
|
||||
*
|
||||
* These tests verify the complete functionality including
|
||||
* - REST API endpoints
|
||||
* - Database operations
|
||||
* - Event publishing
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb",
|
||||
"spring.kafka.bootstrap-servers=localhost:9092"
|
||||
])
|
||||
@ContextConfiguration(classes = [MemberServiceIntegrationTest.TestConfig::class])
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class MemberServiceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
@Qualifier("memberRepositoryImpl")
|
||||
private lateinit var memberRepository: MemberRepository
|
||||
|
||||
@Configuration
|
||||
class TestConfig {
|
||||
@Bean
|
||||
fun eventPublisher(): EventPublisher = mockk(relaxed = true)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() = runBlocking {
|
||||
// Clean up database before each test
|
||||
// Note: In a real implementation, you might want to use @Transactional or @DirtiesContext
|
||||
println("[DEBUG_LOG] Setting up test - cleaning database")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should create member successfully`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing member creation")
|
||||
|
||||
// Given
|
||||
val createRequest = MemberController.CreateMemberRequest(
|
||||
firstName = "John",
|
||||
lastName = "Doe",
|
||||
email = "john.doe@example.com",
|
||||
phone = "+43123456789",
|
||||
dateOfBirth = LocalDate(1990, 1, 15),
|
||||
membershipNumber = "M001",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
membershipEndDate = null,
|
||||
isActive = true,
|
||||
address = "123 Test Street, Vienna",
|
||||
emergencyContact = "Jane Doe: +43987654321"
|
||||
)
|
||||
|
||||
// When
|
||||
val member = Member(
|
||||
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
|
||||
)
|
||||
|
||||
val savedMember = memberRepository.save(member)
|
||||
|
||||
// Then
|
||||
assertNotNull(savedMember)
|
||||
assertEquals(createRequest.firstName, savedMember.firstName)
|
||||
assertEquals(createRequest.lastName, savedMember.lastName)
|
||||
assertEquals(createRequest.email, savedMember.email)
|
||||
assertEquals(createRequest.membershipNumber, savedMember.membershipNumber)
|
||||
assertTrue(savedMember.isActive)
|
||||
|
||||
println("[DEBUG_LOG] Member created successfully with ID: ${savedMember.memberId}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find member by membership number`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find member by membership number")
|
||||
|
||||
// Given
|
||||
val member = Member(
|
||||
firstName = "Jane",
|
||||
lastName = "Smith",
|
||||
email = "jane.smith@example.com",
|
||||
membershipNumber = "M002",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
isActive = true
|
||||
)
|
||||
memberRepository.save(member)
|
||||
|
||||
// When
|
||||
val foundMember = memberRepository.findByMembershipNumber("M002")
|
||||
|
||||
// Then
|
||||
assertNotNull(foundMember)
|
||||
assertEquals("Jane", foundMember.firstName)
|
||||
assertEquals("Smith", foundMember.lastName)
|
||||
assertEquals("M002", foundMember.membershipNumber)
|
||||
|
||||
println("[DEBUG_LOG] Member found by membership number: ${foundMember.memberId}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find member by email`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find member by email")
|
||||
|
||||
// Given
|
||||
val member = Member(
|
||||
firstName = "Bob",
|
||||
lastName = "Johnson",
|
||||
email = "bob.johnson@example.com",
|
||||
membershipNumber = "M003",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
isActive = true
|
||||
)
|
||||
memberRepository.save(member)
|
||||
|
||||
// When
|
||||
val foundMember = memberRepository.findByEmail("bob.johnson@example.com")
|
||||
|
||||
// Then
|
||||
assertNotNull(foundMember)
|
||||
assertEquals("Bob", foundMember.firstName)
|
||||
assertEquals("Johnson", foundMember.lastName)
|
||||
assertEquals("bob.johnson@example.com", foundMember.email)
|
||||
|
||||
println("[DEBUG_LOG] Member found by email: ${foundMember.memberId}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should count active members`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing count active members")
|
||||
|
||||
// Given
|
||||
val activeMember = Member(
|
||||
firstName = "Active",
|
||||
lastName = "Member",
|
||||
email = "active@example.com",
|
||||
membershipNumber = "M004",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
isActive = true
|
||||
)
|
||||
|
||||
val inactiveMember = Member(
|
||||
firstName = "Inactive",
|
||||
lastName = "Member",
|
||||
email = "inactive@example.com",
|
||||
membershipNumber = "M005",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
isActive = false
|
||||
)
|
||||
|
||||
memberRepository.save(activeMember)
|
||||
memberRepository.save(inactiveMember)
|
||||
|
||||
// When
|
||||
val activeCount = memberRepository.countActive()
|
||||
val totalCount = memberRepository.countAll()
|
||||
|
||||
// Then
|
||||
assertTrue(activeCount >= 1, "Should have at least 1 active member")
|
||||
assertTrue(totalCount >= 2, "Should have at least 2 total members")
|
||||
|
||||
println("[DEBUG_LOG] Active members: $activeCount, Total members: $totalCount")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should validate duplicate membership number`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing duplicate membership number validation")
|
||||
|
||||
// Given
|
||||
val member1 = Member(
|
||||
firstName = "First",
|
||||
lastName = "Member",
|
||||
email = "first@example.com",
|
||||
membershipNumber = "M006",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
isActive = true
|
||||
)
|
||||
memberRepository.save(member1)
|
||||
|
||||
// When
|
||||
val exists = memberRepository.existsByMembershipNumber("M006")
|
||||
|
||||
// Then
|
||||
assertTrue(exists, "Should detect existing membership number")
|
||||
|
||||
println("[DEBUG_LOG] Duplicate membership number validation passed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should validate duplicate email`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing duplicate email validation")
|
||||
|
||||
// Given
|
||||
val member = Member(
|
||||
firstName = "Email",
|
||||
lastName = "Test",
|
||||
email = "email.test@example.com",
|
||||
membershipNumber = "M007",
|
||||
membershipStartDate = LocalDate(2024, 1, 1),
|
||||
isActive = true
|
||||
)
|
||||
memberRepository.save(member)
|
||||
|
||||
// When
|
||||
val exists = memberRepository.existsByEmail("email.test@example.com")
|
||||
|
||||
// Then
|
||||
assertTrue(exists, "Should detect existing email")
|
||||
|
||||
println("[DEBUG_LOG] Duplicate email validation passed")
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user