fixing web-app

This commit is contained in:
stefan
2025-09-24 14:21:57 +02:00
parent cd2b0796a6
commit 1c4184809a
156 changed files with 440 additions and 1708 deletions
@@ -0,0 +1,11 @@
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)
}
@@ -0,0 +1,239 @@
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(".")
}
}
@@ -0,0 +1,84 @@
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}"
)
)
}
}
}
@@ -0,0 +1,71 @@
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}"
)
)
}
}
}
@@ -0,0 +1,93 @@
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}"
)
)
}
}
}
@@ -0,0 +1,131 @@
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}"
)
)
}
}
}
@@ -0,0 +1,226 @@
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(".")
}
}
@@ -0,0 +1,146 @@
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
}
}