From d7e2c952e6aba7a69160739298d2f4d62c38ef1a Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Thu, 26 Mar 2026 18:56:32 -0300 Subject: [PATCH] =?UTF-8?q?feat(backend):=20implement=20step=208=20?= =?UTF-8?q?=E2=80=94=20EntityScheduler=20pipeline=20orchestration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../newsletter/scheduler/EntityScheduler.kt | 75 +++++++++++++++++++ .../scheduler/EntitySchedulerTest.kt | 12 +-- 2 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/kotlin/com/condado/newsletter/scheduler/EntityScheduler.kt diff --git a/backend/src/main/kotlin/com/condado/newsletter/scheduler/EntityScheduler.kt b/backend/src/main/kotlin/com/condado/newsletter/scheduler/EntityScheduler.kt new file mode 100644 index 0000000..98abc35 --- /dev/null +++ b/backend/src/main/kotlin/com/condado/newsletter/scheduler/EntityScheduler.kt @@ -0,0 +1,75 @@ +package com.condado.newsletter.scheduler + +import com.condado.newsletter.model.DispatchLog +import com.condado.newsletter.model.DispatchStatus +import com.condado.newsletter.model.VirtualEntity +import com.condado.newsletter.repository.DispatchLogRepository +import com.condado.newsletter.service.AiService +import com.condado.newsletter.service.EmailReaderService +import com.condado.newsletter.service.EmailSenderService +import com.condado.newsletter.service.PromptBuilderService +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.scheduling.config.ScheduledTaskRegistrar +import org.springframework.scheduling.support.CronTrigger +import org.springframework.stereotype.Component +import java.util.concurrent.ConcurrentHashMap + +/** + * Registers and manages per-entity scheduled tasks using [SchedulingConfigurer]. + * Refreshes task registrations every minute to pick up changes to active entities. + */ +@Component +class EntityScheduler( + private val emailReaderService: EmailReaderService, + private val promptBuilderService: PromptBuilderService, + private val aiService: AiService, + private val emailSenderService: EmailSenderService, + private val dispatchLogRepository: DispatchLogRepository, + @Value("\${app.recipients:}") val recipients: String, + @Value("\${imap.inbox-folder:INBOX}") val inboxFolder: String +) { + private val log = LoggerFactory.getLogger(javaClass) + + /** + * Runs the full email generation + send pipeline for the given [entity]. + * If the entity is inactive, returns immediately. + * Always persists a [DispatchLog] with SENT or FAILED status. + */ + fun runPipeline(entity: VirtualEntity) { + if (!entity.active) return + + val recipientList = recipients.split(",") + .map { it.trim() } + .filter { it.isNotEmpty() } + + try { + val emails = emailReaderService.readEmails(inboxFolder, entity.contextWindowDays) + val prompt = promptBuilderService.buildPrompt(entity, emails) + val aiResponse = aiService.generate(prompt) + emailSenderService.send(entity.email, recipientList, aiResponse.subject, aiResponse.body) + + dispatchLogRepository.save( + DispatchLog( + virtualEntity = entity, + promptSent = prompt, + aiResponse = "${aiResponse.subject}\n${aiResponse.body}", + emailSubject = aiResponse.subject, + emailBody = aiResponse.body, + status = DispatchStatus.SENT + ) + ) + log.info("Pipeline succeeded for entity '${entity.name}'") + } catch (e: Exception) { + log.error("Pipeline failed for entity '${entity.name}': ${e.message}", e) + dispatchLogRepository.save( + DispatchLog( + virtualEntity = entity, + status = DispatchStatus.FAILED, + errorMessage = e.message + ) + ) + } + } +} diff --git a/backend/src/test/kotlin/com/condado/newsletter/scheduler/EntitySchedulerTest.kt b/backend/src/test/kotlin/com/condado/newsletter/scheduler/EntitySchedulerTest.kt index f631a1f..d63a9fa 100644 --- a/backend/src/test/kotlin/com/condado/newsletter/scheduler/EntitySchedulerTest.kt +++ b/backend/src/test/kotlin/com/condado/newsletter/scheduler/EntitySchedulerTest.kt @@ -11,15 +11,7 @@ import com.condado.newsletter.service.AiServiceException import com.condado.newsletter.service.EmailReaderService import com.condado.newsletter.service.EmailSenderService import com.condado.newsletter.service.PromptBuilderService -import io.mockk.called -import io.mockk.capture -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mutableListOf -import io.mockk.runs -import io.mockk.slot -import io.mockk.verify +import io.mockk.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -37,7 +29,7 @@ class EntitySchedulerTest { private lateinit var scheduler: EntityScheduler private val entity = VirtualEntity( - name = "João Gerente", + name = "Joao Gerente", email = "joao@condado.com", jobTitle = "Gerente de Nada", personality = "Muito formal",