fixing web-app
This commit is contained in:
@@ -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)
|
||||
}
|
||||
+239
@@ -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(".")
|
||||
}
|
||||
}
|
||||
+84
@@ -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}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -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}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
@@ -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}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
@@ -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}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+226
@@ -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(".")
|
||||
}
|
||||
}
|
||||
+146
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user