Defines the {@link ProducerFactory} and {@link KafkaTemplate} used to publish {@link DomainEvent} envelopes + * keyed by the Note id. Serializers are driven by the {@code spring.kafka.producer} configuration + * ({@code UUIDSerializer} for the key, {@code JacksonJsonSerializer} for the value).
+ */ +@Configuration +public class KafkaConfiguration { + + @Bean + public ProducerFactoryThe topic name follows the FOLIO convention {@code {env}.{tenant}.notes.note} (for example + * {@code folio.diku.notes.note}), resolved via {@link KafkaUtils#getTenantTopicName(String, String)} the same way the + * topic is created on tenant enable. The tenant is resolved at publish time via {@link FolioExecutionContext}.
+ * + *Every message carries headers ({@code X-Okapi-Tenant}, {@code eventType}, {@code domain}) so consumers can + * filter by tenant, event type or Note domain without deserializing the JSON payload.
+ */ +@Slf4j +@Component +public class NoteEventProducer { + + private static final String EVENT_TYPE_HEADER = "eventType"; + private static final String EVENT_DOMAIN_HEADER = "domain"; + + private final KafkaTemplateCallers use the event-specific methods ({@link #publishNoteCreatedEvent(Note)}, + * {@link #publishNoteUpdatedEvent(Note, Note)}, {@link #publishNoteDeletedEvent(Note)}) and only need to provide the + * relevant Note snapshot(s); this service takes care of resolving the current tenant via {@link FolioExecutionContext}, + * choosing the Kafka record key and populating the {@code old}/{@code new} envelope fields for each event type.
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DomainEventPublisherService { + + private final NoteEventProducer noteEventProducer; + private final FolioExecutionContext context; + + /** + * Publishes a {@code CREATE} Note event carrying the new snapshot. + * + * @param newNote the newly-created Note snapshot + */ + public void publishNoteCreatedEvent(Note newNote) { + publish(DomainEvent.createEvent(newNote.getId(), newNote, context.getTenantId())); + } + + /** + * Publishes an {@code UPDATE} Note event carrying both the pre- and post-change snapshots. + * + * @param oldNote the pre-change Note snapshot + * @param newNote the post-change Note snapshot + */ + public void publishNoteUpdatedEvent(Note oldNote, Note newNote) { + publish(DomainEvent.updateEvent(newNote.getId(), oldNote, newNote, context.getTenantId())); + } + + /** + * Publishes a {@code DELETE} Note event carrying the pre-delete snapshot. + * + * @param oldNote the pre-delete Note snapshot + */ + public void publishNoteDeletedEvent(Note oldNote) { + publish(DomainEvent.deleteEvent(oldNote.getId(), oldNote, context.getTenantId())); + } + + private void publish(DomainEventThe consumer owns its listener container and the record buffer; callers only need to + * {@link #subscribe(String, KafkaProperties)} it, {@link #poll(Duration)} the buffered records, and {@link #close()} + * it when done (it is {@link Closeable}, so it also works with try-with-resources or an {@code @AfterEach} hook).
+ */ +public final class TestKafkaConsumer implements Closeable { + + private static final Duration DEFAULT_POLL_TIMEOUT = Duration.ofMinutes(1); + private static final Duration POLL_INTERVAL = Duration.ofSeconds(1); + + private final KafkaMessageListenerContainer