diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c14a14b86..935084a25 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: run: chmod +x gradlew - name: Generate Allure Results - run: ./gradlew test + run: ./gradlew generateAllureResults - name: Load test report history uses: actions/checkout@v3 diff --git a/api/build.gradle.kts b/api/build.gradle.kts index cf14558b5..5a365f4ab 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -334,4 +334,9 @@ tasks.register("buildPinpointEcsDockerImagePrd") { ) } } +} + +tasks.withType(Test::class.java) { + useJUnitPlatform() + systemProperty("allure.results.directory", "$projectDir/build/allure-results") } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d97f62ce5..0e63b0ffc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -178,20 +178,6 @@ subprojects { includeCompileClasspath = false } - tasks { - test { - useJUnitPlatform() - systemProperty("allure.results.directory", "$projectDir/build/allure-results") - } - - register("architectureSpecTest") { - group = "spec" - useJUnitPlatform { - includeTags("ArchitectureSpec") - } - } - } - /** server url */ val serverUrl = project.hasProperty("serverUrl").let { @@ -265,4 +251,14 @@ tasks.named("gitExecutableHooks").configure { tasks.named("clean").configure { dependsOn("gitExecutableHooks") +} + +tasks.withType(Test::class.java) { + useJUnitPlatform() + systemProperty("allure.results.directory", "$projectDir/build/allure-results") +} + +tasks.register("generateAllureResults") { + dependsOn("api:test") + dependsOn("domain:crm:test") } \ No newline at end of file diff --git a/domain/crm/build.gradle.kts b/domain/crm/build.gradle.kts index 1a49dcd17..a592e5882 100644 --- a/domain/crm/build.gradle.kts +++ b/domain/crm/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(project(":library:web")) implementation(project(":library:email")) implementation(project(":library:event")) + testImplementation(testFixtures(project(":library:event"))) /** jsoup - html parser */ implementation("org.jsoup:jsoup:1.15.3") @@ -37,4 +38,18 @@ vaadin { tasks.named("bootJar") { dependsOn("vaadinBuildFrontend") +} + +tasks.withType(Test::class.java) { + useJUnitPlatform() + systemProperty("allure.results.directory", "$projectDir/build/allure-results") + + doLast { + val allureResults = File("$projectDir/build/allure-results") + val results = File("$rootDir/api/build/allure-results") + if (!results.exists()) { + results.mkdirs() + } + allureResults.copyRecursively(results, true) + } } \ No newline at end of file diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/domain/EmailTemplate.kt b/domain/crm/src/main/kotlin/com/few/crm/email/domain/EmailTemplate.kt index a7fb09088..d6ac35342 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/domain/EmailTemplate.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/domain/EmailTemplate.kt @@ -1,15 +1,17 @@ package com.few.crm.email.domain +import com.few.crm.email.event.template.PostEmailTemplateEvent import com.few.crm.support.jpa.converter.StringListConverter import jakarta.persistence.* import org.springframework.data.annotation.CreatedDate +import org.springframework.data.domain.AbstractAggregateRoot import org.springframework.data.jpa.domain.support.AuditingEntityListener import java.time.LocalDateTime @Entity @Table(name = "email_templates") @EntityListeners(AuditingEntityListener::class) -data class EmailTemplate( +class EmailTemplate( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long? = null, @@ -27,7 +29,7 @@ data class EmailTemplate( var version: Float = 1.0f, @CreatedDate var createdAt: LocalDateTime? = null, -) { +) : AbstractAggregateRoot() { protected constructor() : this( templateName = "", subject = "", @@ -50,7 +52,15 @@ data class EmailTemplate( ) } - fun isNewTemplate(): Boolean = id == null + fun isNewTemplate(): Boolean { + registerEvent( + PostEmailTemplateEvent( + templateId = this.id ?: 0, + eventType = "POST", + ), + ) + return id == null + } fun modifySubject(subject: String?): EmailTemplate { subject?.let { @@ -77,6 +87,13 @@ data class EmailTemplate( } ?: kotlin.run { this.version += 0.1f } + + registerEvent( + PostEmailTemplateEvent( + templateId = this.id!!, + eventType = "POST", + ), + ) return this } } \ No newline at end of file diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/domain/ScheduledEvent.kt b/domain/crm/src/main/kotlin/com/few/crm/email/domain/ScheduledEvent.kt index 0097d461c..91b9caf50 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/domain/ScheduledEvent.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/domain/ScheduledEvent.kt @@ -4,7 +4,15 @@ import jakarta.persistence.* import org.springframework.data.jpa.domain.support.AuditingEntityListener @Entity -@Table(name = "scheduled_events") +@Table( + name = "scheduled_events", + uniqueConstraints = [ + UniqueConstraint( + name = "event_id_unique", + columnNames = ["event_id"], + ), + ], +) @EntityListeners(AuditingEntityListener::class) data class ScheduledEvent( @Id diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/domain/SentEmail.kt b/domain/crm/src/main/kotlin/com/few/crm/email/domain/SentEmail.kt new file mode 100644 index 000000000..01c2bf9f0 --- /dev/null +++ b/domain/crm/src/main/kotlin/com/few/crm/email/domain/SentEmail.kt @@ -0,0 +1,24 @@ +package com.few.crm.email.domain + +import com.few.crm.email.event.send.EmailSentEvent +import org.springframework.data.domain.AbstractAggregateRoot + +class SentEmail( + private val userExternalId: String, + private val emailBody: String, + private val destination: String, + private val emailMessageId: String, + private val eventType: EmailSendEventType = EmailSendEventType.SEND, +) : AbstractAggregateRoot() { + init { + registerEvent( + EmailSentEvent( + userExternalId = userExternalId, + emailBody = emailBody, + messageId = emailMessageId, + destination = destination, + eventType = eventType.name, + ), + ) + } +} \ No newline at end of file diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/event/schedule/ScheduledEvent.kt b/domain/crm/src/main/kotlin/com/few/crm/email/event/schedule/ScheduledEvent.kt index af3158e73..665380d4c 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/event/schedule/ScheduledEvent.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/event/schedule/ScheduledEvent.kt @@ -1,6 +1,7 @@ package com.few.crm.email.event.schedule import event.Event +import event.EventDetails import event.EventUtils import java.time.LocalDateTime @@ -14,6 +15,7 @@ abstract class ScheduledEvent( eventTime, ) +@EventDetails(publishedLocations = ["com.few.crm.support.schedule.TimeOutEventTaskManager"]) class CancelScheduledEvent( val targetEventId: String, eventId: String = EventUtils.generateEventId(), diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/EmailSendEvent.kt b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/EmailSendEvent.kt index 9fa899ce6..08b8e86b8 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/EmailSendEvent.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/EmailSendEvent.kt @@ -40,7 +40,10 @@ abstract class EmailSendEvent( override fun toString(): String = "EmailSendEvent(messageId='$messageId', destination='$destination', timestamp=$timestamp)" } -@EventDetails(outBox = false) +@EventDetails( + outBox = false, + publishedLocations = ["com.few.crm.email.domain.SentEmail", "com.few.crm.email.event.send.handler.NotificationEmailSendTimeOutInvokeEventHandler"], +) class EmailSentEvent( val userExternalId: String, val emailBody: String, @@ -59,6 +62,7 @@ class EmailSentEvent( timestamp = timestamp, ) +@EventDetails(outBox = false, publishedLocations = ["com.few.crm.email.relay.send.EmailSendEventMessageMapper"]) class EmailDeliveryEvent( eventId: String, eventType: String, @@ -75,6 +79,7 @@ class EmailDeliveryEvent( timestamp = timestamp, ) +@EventDetails(outBox = false, publishedLocations = ["com.few.crm.email.relay.send.EmailSendEventMessageMapper"]) class EmailOpenEvent( eventId: String, eventType: String, @@ -91,6 +96,7 @@ class EmailOpenEvent( timestamp = timestamp, ) +@EventDetails(outBox = false, publishedLocations = ["com.few.crm.email.relay.send.EmailSendEventMessageMapper"]) class EmailClickEvent( eventId: String, eventType: String, @@ -107,6 +113,7 @@ class EmailClickEvent( timestamp = timestamp, ) +@EventDetails(outBox = false, publishedLocations = ["com.few.crm.email.relay.send.EmailSendEventMessageMapper"]) class EmailDeliveryDelayEvent( eventId: String, eventType: String, diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/NotificationEmailSendTimeOutEvent.kt b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/NotificationEmailSendTimeOutEvent.kt index 11a17c062..dee153e44 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/NotificationEmailSendTimeOutEvent.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/NotificationEmailSendTimeOutEvent.kt @@ -1,11 +1,15 @@ package com.few.crm.email.event.send +import event.EventDetails import event.EventUtils import event.TimeExpiredEvent import event.TimeOutEvent import org.springframework.context.ApplicationEventPublisher import java.time.LocalDateTime +@EventDetails( + publishedLocations = ["com.few.crm.email.event.send.replayer.NotificationEmailSendTimeOutEventReplayer", "com.few.crm.view.email.CrmEmailSendView"], +) open class NotificationEmailSendTimeOutEvent( val templateId: Long, val userIds: List, @@ -66,6 +70,9 @@ open class NotificationEmailSendTimeOutEvent( ) } +@EventDetails( + publishedLocations = ["com.few.crm.email.event.send.NotificationEmailSendTimeOutInvokeEvent", "com.few.crm.email.relay.send.aws.ScheduledEventReverseRelay"], +) class NotificationEmailSendTimeOutInvokeEvent( val templateId: Long, val userIds: List, @@ -80,6 +87,7 @@ class NotificationEmailSendTimeOutInvokeEvent( eventTime, ) +@EventDetails(publishedLocations = ["com.few.crm.email.event.send.NotificationEmailSendTimeOutEvent"]) class AwsNotificationEmailSendTimeOutEvent( templateId: Long, userIds: List, diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/handler/NotificationEmailSendTimeOutInvokeEventHandler.kt b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/handler/NotificationEmailSendTimeOutInvokeEventHandler.kt index 0baa80511..6c7fcf480 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/handler/NotificationEmailSendTimeOutInvokeEventHandler.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/handler/NotificationEmailSendTimeOutInvokeEventHandler.kt @@ -27,7 +27,9 @@ class NotificationEmailSendTimeOutInvokeEventHandler( ) : EventHandler { @CrmTransactional override fun handle(event: NotificationEmailSendTimeOutInvokeEvent) { - scheduledEventRepository.findByEventId(event.timeOutEventId)?.complete() + scheduledEventRepository + .findByEventIdAndCompletedFalseForUpdate(event.timeOutEventId) + ?.complete() ?: return val templateId = event.templateId val userIds = event.userIds val template = diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/replayer/NotificationEmailSendTimeOutEventReplayer.kt b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/replayer/NotificationEmailSendTimeOutEventReplayer.kt index 268206abd..9bbbb17b8 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/event/send/replayer/NotificationEmailSendTimeOutEventReplayer.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/event/send/replayer/NotificationEmailSendTimeOutEventReplayer.kt @@ -40,9 +40,18 @@ class NotificationEmailSendTimeOutEventReplayer( } override fun replay() { - log.info { "==================== [START] NotificationEmailSendTimeOutEventReplayer ====================" } + val logBuffer = StringBuilder() + logBuffer.appendLine("NotificationEmailSendTimeOutEventReplayer:") var expiredEventCount = 0L var replayedEventCount = 0L + val expiredEventsLogBuffer = + StringBuilder().apply { + appendLine("\\-Expired events:") + } + val replayedEventsLogBuffer = + StringBuilder().apply { + appendLine("\\-Replayed events:") + } eventScheduleRepository .findAllByEventClassAndCompletedFalse(NotificationEmailSendTimeOutEvent::class.simpleName.toString()) .filter { @@ -62,18 +71,20 @@ class NotificationEmailSendTimeOutEventReplayer( ) if (event.isExpired()) { // TODO alert - log.error { "Event is expired. eventId: ${event.eventId} expiredTime: ${event.expiredTime}" } + expiredEventsLogBuffer.appendLine(" - eventId: ${event.eventId} expiredTime: ${event.expiredTime}") eventScheduleRepository.findByEventId(event.eventId)?.isNotConsumed()?.complete() expiredEventCount++ return@forEach } - log.info { "Event is replayed. eventId: ${event.eventId} expiredTime: ${event.expiredTime}" } + replayedEventsLogBuffer.appendLine(" - eventId: ${event.eventId} expiredTime: ${event.expiredTime}") timeOutEventTaskManager.reSchedule(event) replayedEventCount++ } } - log.info { "Expired event count: $expiredEventCount" } - log.info { "Replayed event count: $replayedEventCount" } - log.info { "==================== [END] NotificationEmailSendTimeOutEventReplayer ====================" } + logBuffer.append(expiredEventsLogBuffer) + logBuffer.append(replayedEventsLogBuffer) + logBuffer.appendLine("Expired event count: $expiredEventCount") + logBuffer.append("Replayed event count: $replayedEventCount") + log.info { logBuffer.toString() } } } \ No newline at end of file diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/event/template/EmailTemplateTransactionEvent.kt b/domain/crm/src/main/kotlin/com/few/crm/email/event/template/EmailTemplateTransactionEvent.kt index 24c58e5a8..2cef92e86 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/event/template/EmailTemplateTransactionEvent.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/event/template/EmailTemplateTransactionEvent.kt @@ -1,6 +1,7 @@ package com.few.crm.email.event.template import event.Event +import event.EventDetails import event.EventUtils import java.time.LocalDateTime @@ -24,6 +25,7 @@ abstract class EmailTemplateTransactionAfterCompletionEvent( eventTime = eventTime, ) +@EventDetails(publishedLocations = ["com.few.crm.email.domain.EmailTemplate"]) class PostEmailTemplateEvent( val templateId: Long, eventId: String = EventUtils.generateEventId(), diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/repository/ScheduledEventRepository.kt b/domain/crm/src/main/kotlin/com/few/crm/email/repository/ScheduledEventRepository.kt index 786e7e2d8..fe47be277 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/repository/ScheduledEventRepository.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/repository/ScheduledEventRepository.kt @@ -1,11 +1,18 @@ package com.few.crm.email.repository import com.few.crm.email.domain.ScheduledEvent +import jakarta.persistence.LockModeType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock +import org.springframework.data.jpa.repository.Query interface ScheduledEventRepository : JpaRepository { fun findByEventId(eventId: String): ScheduledEvent? + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT e FROM ScheduledEvent e WHERE e.eventId = :eventId AND e.completed = false") + fun findByEventIdAndCompletedFalseForUpdate(eventId: String): ScheduledEvent? + fun findAllByCompletedFalse(): List fun findAllByEventClassAndCompletedFalse(eventClass: String): List diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/service/CrmSendNonVariablesEmailService.kt b/domain/crm/src/main/kotlin/com/few/crm/email/service/CrmSendNonVariablesEmailService.kt index dfe4ce5ee..f2bdd8e4c 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/service/CrmSendNonVariablesEmailService.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/service/CrmSendNonVariablesEmailService.kt @@ -1,7 +1,10 @@ package com.few.crm.email.service +import com.few.crm.email.domain.EmailSendEventType +import com.few.crm.email.domain.SentEmail import email.* import email.provider.CrmAwsSESEmailSendProvider +import event.domain.PublishEvents import org.springframework.boot.autoconfigure.mail.MailProperties import org.springframework.stereotype.Component @@ -13,6 +16,20 @@ data class SendEmailArgs( override val properties: String = "", ) : SendMailArgs +data class SendEmailDto( + val to: String, + val subject: String, + val template: String, + val content: NonContent, + val properties: String = "", + val userExternalId: String, + val emailBody: String, + val destination: String, + val eventType: EmailSendEventType, +) { + val emailArgs = SendEmailArgs(to, subject, template, content, properties) +} + @Component class CrmSendNonVariablesEmailService( mailProperties: MailProperties, @@ -23,6 +40,18 @@ class CrmSendNonVariablesEmailService( val context = EmailContext() return emailTemplateProcessor.process(args.template, context, EmailTemplateType.STRING) } + + @PublishEvents + fun send(args: SendEmailDto): SentEmail { + val emailArgs = args.emailArgs + return SentEmail( + userExternalId = args.userExternalId, + emailBody = args.emailBody, + destination = args.destination, + emailMessageId = send(emailArgs), + eventType = args.eventType, + ) + } } class NonContent \ No newline at end of file diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/usecase/PostTemplateUseCase.kt b/domain/crm/src/main/kotlin/com/few/crm/email/usecase/PostTemplateUseCase.kt index 65c72a3cb..5a2aca7f2 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/usecase/PostTemplateUseCase.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/usecase/PostTemplateUseCase.kt @@ -1,19 +1,16 @@ package com.few.crm.email.usecase import com.few.crm.email.domain.EmailTemplate -import com.few.crm.email.event.template.PostEmailTemplateEvent import com.few.crm.email.repository.EmailTemplateRepository import com.few.crm.email.service.HtmlValidator import com.few.crm.email.usecase.dto.PostTemplateUseCaseIn import com.few.crm.email.usecase.dto.PostTemplateUseCaseOut import com.few.crm.support.jpa.CrmTransactional -import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service @Service class PostTemplateUseCase( private val emailTemplateRepository: EmailTemplateRepository, - private val applicationEventPublisher: ApplicationEventPublisher, private val htmlValidator: HtmlValidator, ) { @CrmTransactional @@ -64,13 +61,6 @@ class PostTemplateUseCase( } } - applicationEventPublisher.publishEvent( - PostEmailTemplateEvent( - templateId = modifiedOrNewTemplate.id!!, - eventType = "POST", - ), - ) - return run { PostTemplateUseCaseOut( id = modifiedOrNewTemplate.id!!, diff --git a/domain/crm/src/main/kotlin/com/few/crm/email/usecase/SendNotificationEmailUseCase.kt b/domain/crm/src/main/kotlin/com/few/crm/email/usecase/SendNotificationEmailUseCase.kt index 307710cd5..ef49873c5 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/email/usecase/SendNotificationEmailUseCase.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/email/usecase/SendNotificationEmailUseCase.kt @@ -2,16 +2,14 @@ package com.few.crm.email.usecase import com.fasterxml.jackson.databind.ObjectMapper import com.few.crm.email.domain.EmailSendEventType -import com.few.crm.email.event.send.EmailSentEvent import com.few.crm.email.repository.EmailTemplateHistoryRepository import com.few.crm.email.repository.EmailTemplateRepository import com.few.crm.email.service.CrmSendNonVariablesEmailService import com.few.crm.email.service.NonContent -import com.few.crm.email.service.SendEmailArgs +import com.few.crm.email.service.SendEmailDto import com.few.crm.email.usecase.dto.SendNotificationEmailUseCaseIn import com.few.crm.email.usecase.dto.SendNotificationEmailUseCaseOut import com.few.crm.user.repository.UserRepository -import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service data class NotificationEmailTemplateProperties( @@ -25,7 +23,6 @@ class SendNotificationEmailUseCase( private val emailTemplateHistoryRepository: EmailTemplateHistoryRepository, private val userRepository: UserRepository, private val crmSendNonVariablesEmailService: CrmSendNonVariablesEmailService, - private val applicationEventPublisher: ApplicationEventPublisher, private val objectMapper: ObjectMapper, ) { fun execute(useCaseIn: SendNotificationEmailUseCaseIn): SendNotificationEmailUseCaseOut { @@ -42,23 +39,16 @@ class SendNotificationEmailUseCase( } targetUsers.keys.forEach { email -> - val emailMessageId = - crmSendNonVariablesEmailService.send( - SendEmailArgs( - to = email, - subject = properties.subject, - template = properties.body, - content = NonContent(), - ), - ) - - applicationEventPublisher.publishEvent( - EmailSentEvent( + crmSendNonVariablesEmailService.send( + SendEmailDto( + to = email, + subject = properties.subject, + template = properties.body, + content = NonContent(), userExternalId = targetUsers[email]!!.first().externalId!!, emailBody = properties.body, destination = email, - messageId = emailMessageId, - eventType = EmailSendEventType.SEND.name, + eventType = EmailSendEventType.SEND, ), ) } diff --git a/domain/crm/src/main/kotlin/com/few/crm/support/schedule/aws/AwsSchedulerService.kt b/domain/crm/src/main/kotlin/com/few/crm/support/schedule/aws/AwsSchedulerService.kt index 34ca3d531..845aca35d 100644 --- a/domain/crm/src/main/kotlin/com/few/crm/support/schedule/aws/AwsSchedulerService.kt +++ b/domain/crm/src/main/kotlin/com/few/crm/support/schedule/aws/AwsSchedulerService.kt @@ -24,6 +24,7 @@ fun LocalDateTime.toScheduleExpression(): String = @Service class AwsSchedulerService( + // TODO fix the dependency private val scheduledEventRepository: ScheduledEventRepository, private val awsSchedulerClient: SchedulerClient, private val objectMapper: ObjectMapper, diff --git a/domain/crm/src/test/kotlin/com/few/crm/document/CrmDocument.kt b/domain/crm/src/test/kotlin/com/few/crm/document/CrmDocument.kt new file mode 100644 index 000000000..76b2576b7 --- /dev/null +++ b/domain/crm/src/test/kotlin/com/few/crm/document/CrmDocument.kt @@ -0,0 +1,123 @@ +package com.few.crm.document + +import com.tngtech.archunit.core.domain.JavaClass +import com.tngtech.archunit.core.domain.JavaModifier +import com.tngtech.archunit.core.importer.ClassFileImporter +import event.Event +import event.EventDetails +import io.qameta.allure.Allure +import io.qameta.allure.Epic +import io.qameta.allure.Feature +import io.qameta.allure.Link +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.modulith.core.ApplicationModules +import org.springframework.modulith.docs.Documenter +import java.io.File + +@Epic("V2.0 CRM") +@Feature("Document") +@Link("https://thetimetube.herokuapp.com/asciidoc/") +class CrmDocument { + @Nested + inner class DependencyDiagram { + @Test + fun `create dependency diagram`() { + val modules = ApplicationModules.of("com.few.crm") + Documenter(modules) + .writeDocumentation() + .writeIndividualModulesAsPlantUml() + + modules + .filterNot { it.name == "config" } + .forEach { + val outputFile = File("build/spring-modulith-docs/module-${it.name}.adoc") + Allure.addAttachment("${it.name} Module", "text/plain", outputFile.readText()) + } + } + } + + @Nested + inner class EventDocument { + @Test + fun `create event document`() { + val classes = ClassFileImporter().importPackages("com.few.crm") + val eventClasses = + classes + .stream() + .filter { it.isAssignableTo(Event::class.java) } + .filter { it.isAnonymousClass.not() } + .filter { it.isInnerClass.not() } + .filter { it.isLocalClass.not() } + .filter { it.modifiers.contains(JavaModifier.ABSTRACT).not() } + .toList() + + val notQualifiedEventClasses = mutableListOf() + val logBuilder = StringBuilder() + + eventClasses.forEach { event -> + if (event.isAnnotatedWith(EventDetails::class.java)) { + val eventDetails = event.getAnnotationOfType(EventDetails::class.java) + val publishedLocations = eventDetails.publishedLocations + if (publishedLocations.isEmpty()) { + notQualifiedEventClasses.add(event) + } else { + publishedLocations + .filter { it != (event.packageName + "." + event.simpleName) } + .forEach { publishedLocation -> + event.directDependenciesToSelf + .find { + it.originClass.fullName == publishedLocation + }?.let { + logBuilder.appendLine("* ${it.originClass.fullName}") + } ?: run { + notQualifiedEventClasses.add(event) + } + } + } + } else { + notQualifiedEventClasses.add(event) + } + } + + if (notQualifiedEventClasses.isNotEmpty()) { + logBuilder.appendLine("\n== Not Qualified Event Classes") + logBuilder.appendLine("_The following event classes are not annotated with @EventDetails:_") + notQualifiedEventClasses.forEach { + logBuilder.appendLine("* ${it.fullName}") + } + throw IllegalStateException(logBuilder.toString()) + } + + // 결과를 .adoc 파일로 저장 + val outputFile = File("build/event-docs/event-published-document.adoc") + if (outputFile.exists()) { + outputFile.delete() + } + outputFile.parentFile.mkdirs() + val adocContent = StringBuilder() + + adocContent.appendLine("[%autowidth.stretch, cols=\"h,a\"]") + adocContent.appendLine("|===") + adocContent.appendLine("|Event Class | Published Locations") + + eventClasses.forEach { event -> + val eventDetails = event.getAnnotationOfType(EventDetails::class.java) + val publishedLocations = eventDetails.publishedLocations + + adocContent.appendLine("|`${event.simpleName}`") + adocContent.appendLine("|") + adocContent.appendLine( + publishedLocations.joinToString("\n") { "* `$it`" }, + ) + } + + adocContent.appendLine("|===") + + outputFile.writeText(adocContent.toString()) + + Allure.addAttachment("Event Document", "text/plain", adocContent.toString()) + println("Event document generated at: ${outputFile.absolutePath}") + } + } +} \ No newline at end of file diff --git a/library/event/src/main/kotlin/event/EventDetails.kt b/library/event/src/main/kotlin/event/EventDetails.kt index 56352f2fb..16bd46b94 100644 --- a/library/event/src/main/kotlin/event/EventDetails.kt +++ b/library/event/src/main/kotlin/event/EventDetails.kt @@ -11,4 +11,5 @@ package event @Retention(AnnotationRetention.RUNTIME) annotation class EventDetails( val outBox: Boolean = false, + val publishedLocations: Array = [], ) \ No newline at end of file diff --git a/library/event/src/main/kotlin/event/domain/PublishEvents.kt b/library/event/src/main/kotlin/event/domain/PublishEvents.kt new file mode 100644 index 000000000..7d1705bde --- /dev/null +++ b/library/event/src/main/kotlin/event/domain/PublishEvents.kt @@ -0,0 +1,5 @@ +package event.domain + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class PublishEvents \ No newline at end of file