From f9d9d01b21c321c41663bc41443f37f0731cb2bf Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Sun, 27 Jul 2025 23:28:39 +0200 Subject: [PATCH] feat(core): Implement initial Shared Kernel foundation Initializes the foundational `core`-module, which will act as the "Shared Kernel" for the entire Meldestelle_Pro ecosystem. This commit establishes the central building blocks required by all other domains. - **Ubiquitous Language:** Defines core enums (`SparteE`, `PferdeGeschlechtE`, `DatenQuelleE`, `RolleE`, `BerechtigungE`) to ensure a consistent language across all services. - **API Consistency:** Implements standardized DTOs (`ApiResponse`, `PagedResponse`, `EntityDto`) to guarantee that all APIs have a uniform structure. - **Domain Events:** Creates the base interfaces (`DomainEvent`, `DomainEventPublisher`) for our event-driven architecture. - **Error Handling & Validation:** Introduces a functional `Result` type for robust error handling and `ValidationResult` for business rule validation within domain entities. - **Serialization:** Provides common serializers for standard data types like `UUID` and `Instant` to ensure data consistency. This foundational module is a critical prerequisite for the development of all subsequent domain services, starting with the `masterdata-service`. --- .../mocode/core/domain/event/DomainEvent.kt | 51 +++++++++++---- .../at/mocode/core/domain/model/BaseDto.kt | 62 +++++++------------ .../at/mocode/core/domain/model/Enums.kt | 29 +++++++-- .../at/mocode/core/utils/error/Result.kt | 37 +++++++++++ .../core/utils/validation/Validation.kt | 25 ++++++++ 5 files changed, 147 insertions(+), 57 deletions(-) create mode 100644 core/core-domain/src/main/kotlin/at/mocode/core/utils/error/Result.kt create mode 100644 core/core-domain/src/main/kotlin/at/mocode/core/utils/validation/Validation.kt diff --git a/core/core-domain/src/main/kotlin/at/mocode/core/domain/event/DomainEvent.kt b/core/core-domain/src/main/kotlin/at/mocode/core/domain/event/DomainEvent.kt index 61cf5157..f334c7cb 100644 --- a/core/core-domain/src/main/kotlin/at/mocode/core/domain/event/DomainEvent.kt +++ b/core/core-domain/src/main/kotlin/at/mocode/core/domain/event/DomainEvent.kt @@ -2,27 +2,32 @@ package at.mocode.core.domain.event import com.benasher44.uuid.Uuid import com.benasher44.uuid.uuid4 +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant /** - * Interface for all domain events in the system. - * Domain events represent something that happened in the domain that domain experts care about. + * Base interface for all domain events in the system. + * A domain event represents something significant that has happened in a specific domain. */ interface DomainEvent { + /** * Unique identifier for this event instance. */ val eventId: Uuid - /** - * Timestamp when the event occurred. - */ - val timestamp: java.time.Instant - /** * Identifier of the aggregate that the event belongs to. */ val aggregateId: Uuid + val eventType: String + + /** + * Timestamp when the event occurred. + */ + val timestamp: Instant + /** * Version of the aggregate after the event was applied. */ @@ -30,12 +35,34 @@ interface DomainEvent { } /** - * Base implementation of the DomainEvent interface. - * Provides default implementations for common properties. + * Abstract base class for domain events to reduce boilerplate code. */ abstract class BaseDomainEvent( - override val eventId: Uuid = uuid4(), - override val timestamp: java.time.Instant = java.time.Instant.now(), override val aggregateId: Uuid, - override val version: Long + + override val eventType: String, + + override val version: Long, + + override val eventId: Uuid = uuid4(), + + override val timestamp: Instant = Clock.System.now() + + ) : DomainEvent + +/** + * Interface for a component that can publish domain events, typically to a message bus like Kafka. + */ +interface DomainEventPublisher { + suspend fun publish(event: DomainEvent) + suspend fun publishAll(events: List) +} + +/** + * Interface for a component that can handle (react to) a specific type of domain event. + */ +interface DomainEventHandler { + suspend fun handle(event: T) + fun canHandle(eventType: String): Boolean +} diff --git a/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/BaseDto.kt b/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/BaseDto.kt index 4ed096c0..b0ecb288 100644 --- a/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/BaseDto.kt +++ b/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/BaseDto.kt @@ -3,6 +3,7 @@ package at.mocode.core.domain.model import at.mocode.core.domain.serialization.KotlinInstantSerializer import at.mocode.core.domain.serialization.UuidSerializer import com.benasher44.uuid.Uuid +import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.Serializable @@ -27,42 +28,32 @@ abstract class EntityDto : BaseDto { } /** - * Standard API response wrapper + * A standardized wrapper for all API responses. + * Provides a consistent structure for data, success status, and errors. + * @param T The type of the data payload. */ @Serializable data class ApiResponse( - val success: Boolean, val data: T? = null, - val error: ErrorDto? = null, - val message: String? = null -) : BaseDto { - companion object { - /** - * Creates a successful API response with data - */ - fun success(data: T, message: String? = null): ApiResponse { - return ApiResponse( - success = true, - data = data, - message = message - ) - } + val success: Boolean = true, + val message: String? = null, + val errors: List = emptyList(), + val timestamp: Instant = Clock.System.now() +) - /** - * Creates an error API response - */ - fun error(message: String, code: String = "ERROR", details: Map? = null): ApiResponse { - return ApiResponse( - success = false, - error = ErrorDto( - code = code, - message = message, - details = details - ) - ) - } - } -} +/** + * A standardized wrapper for paginated API responses. + * @param T The type of the content in the page. + */ +data class PagedResponse( + val content: List, + val page: Int, + val size: Int, + val totalElements: Long, + val totalPages: Int, + val hasNext: Boolean, + val hasPrevious: Boolean +) /** * Error information DTO @@ -85,12 +76,5 @@ data class PaginationDto( val totalPages: Int ) : BaseDto -/** - * Paginated response wrapper - */ -@Serializable -data class PagedResponse( - val data: List, - val pagination: PaginationDto -) : BaseDto + diff --git a/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/Enums.kt b/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/Enums.kt index d9b35f39..34f8a14b 100644 --- a/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/Enums.kt +++ b/core/core-domain/src/main/kotlin/at/mocode/core/domain/model/Enums.kt @@ -3,17 +3,23 @@ package at.mocode.core.domain.model import kotlinx.serialization.Serializable /** - * Data source enumeration - indicates where data originated from + * Defines the source of a data record. */ @Serializable -enum class DatenQuelleE { OEPS_ZNS, MANUELL } +enum class DatenQuelleE { + MANUELL, // Manually entered + IMPORT_ZNS, // Imported from OEPS ZNS data + SYSTEM_GENERATED // Generated by the system itself +} /** - * Horse gender enumeration + * Defines the gender of a horse. */ @Serializable enum class PferdeGeschlechtE { - HENGST, STUTE, WALLACH, UNBEKANNT + HENGST, // Male, not castrated + STUTE, // Female + WALLACH // Male, castrated } /** @@ -23,10 +29,21 @@ enum class PferdeGeschlechtE { enum class GeschlechtE { M, W, D, UNBEKANNT } /** - * Sport discipline enumeration + * Defines the different equestrian disciplines (Sparten). + * This enum is a central part of the Ubiquitous Language. */ @Serializable -enum class SparteE { DRESSUR, SPRINGEN, VIELSEITIGKEIT, FAHREN, VOLTIGIEREN, WESTERN, DISTANZ, ISLAND, PFERDESPORT_SPIEL, BASIS, KOMBINIERT, SONSTIGES } +enum class SparteE { + DRESSUR, // Dressurreiten + SPRINGEN, // Springreiten + VIELSEITIGKEIT, // Vielseitigkeitsreiten + FAHREN, // Fahrsport + VOLTIGIEREN, // Voltigieren + WESTERN, // Westernreiten + DISTANZ, // Distanzreiten + PARA_DRESSUR, // Para-Dressur + ISLAND; // Islandpferde +} /** * Venue/place type enumeration diff --git a/core/core-domain/src/main/kotlin/at/mocode/core/utils/error/Result.kt b/core/core-domain/src/main/kotlin/at/mocode/core/utils/error/Result.kt new file mode 100644 index 00000000..0adff120 --- /dev/null +++ b/core/core-domain/src/main/kotlin/at/mocode/core/utils/error/Result.kt @@ -0,0 +1,37 @@ +package at.mocode.core.utils.error + +/** + * A functional approach to error handling, avoiding exceptions for predictable errors. + * Represents a value that can either be a Success (containing the result) or a Failure (containing an error). + * + * @param T The type of the success value. + * @param E The type of the error value. + */ +sealed class Result { + data class Success(val value: T) : Result() + data class Failure(val error: E) : Result() + + val isSuccess: Boolean get() = this is Success + val isFailure: Boolean get() = this is Failure + + fun getOrNull(): T? = when (this) { + is Success -> value + is Failure -> null + } + + fun getOrElse(defaultValue: @UnsafeVariance T): T = when (this) { + is Success -> value + is Failure -> defaultValue + } +} + +// Extension functions for convenient usage +inline fun Result.onSuccess(action: (T) -> Unit): Result { + if (this is Result.Success) action(value) + return this +} + +inline fun Result.onFailure(action: (E) -> Unit): Result { + if (this is Result.Failure) action(error) + return this +} diff --git a/core/core-domain/src/main/kotlin/at/mocode/core/utils/validation/Validation.kt b/core/core-domain/src/main/kotlin/at/mocode/core/utils/validation/Validation.kt new file mode 100644 index 00000000..ad8ce7e8 --- /dev/null +++ b/core/core-domain/src/main/kotlin/at/mocode/core/utils/validation/Validation.kt @@ -0,0 +1,25 @@ +package at.mocode.core.utils.validation + +/** + * Represents a single validation error. + * @param field The name of the field that failed validation. + * @param message A user-friendly error message. + */ +data class ValidationError( + val field: String, + val message: String +) + +/** + * Represents the result of a validation process. + */ +data class ValidationResult( + val isValid: Boolean, + val errors: List = emptyList() +) { + companion object { + fun valid() = ValidationResult(true) + fun invalid(errors: List) = ValidationResult(false, errors) + fun invalid(field: String, message: String) = ValidationResult(false, listOf(ValidationError(field, message))) + } +}