Repository: urlaubsverwaltung/urlaubsverwaltung
Pull request #4598 (2024-04-01 11:08:06)

Publish vacation type update
closes https://github.com/urlaubsverwaltung/urlaubsverwaltung/issues/4619

sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [19.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598&metric=new_coverage&view=list) (required ≥ 80%)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [C Reliability Rating on New Code](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598) (required ≥ A)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=urlaubsverwaltung_urlaubsverwaltung&pullRequest=4598)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarLint](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)



Patch:
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeCreatedEvent.java b/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeCreatedEvent.java
new file mode 100644
index 000000000..54991ade1
--- /dev/null
+++ b/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeCreatedEvent.java
@@ -0,0 +1,11 @@
+package org.synyx.urlaubsverwaltung.application.vacationtype;
+
+import java.time.Instant;
+import java.util.UUID;
+
+public record VacationTypeCreatedEvent(UUID id, Instant createdAt, VacationType<?> vacationType) {
+
+    public static VacationTypeCreatedEvent of(VacationType<?> vacationType) {
+        return new VacationTypeCreatedEvent(UUID.randomUUID(), Instant.now(), vacationType);
+    }
+}
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImpl.java b/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImpl.java
index fc1c55037..5ed2fa8b0 100644
--- a/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImpl.java
+++ b/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImpl.java
@@ -3,6 +3,7 @@ package org.synyx.urlaubsverwaltung.application.vacationtype;
 import org.slf4j.Logger;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.context.event.ApplicationStartedEvent;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.MessageSource;
 import org.springframework.context.event.EventListener;
 import org.springframework.data.domain.Sort;
@@ -34,11 +35,15 @@ public class VacationTypeServiceImpl implements VacationTypeService {
     private static final Logger LOG = getLogger(lookup().lookupClass());
 
     private final VacationTypeRepository vacationTypeRepository;
+    private final ApplicationEventPublisher applicationEventPublisher;
     private final MessageSource messageSource;
 
     @Autowired
-    VacationTypeServiceImpl(VacationTypeRepository vacationTypeRepository, MessageSource messageSource) {
+    VacationTypeServiceImpl(VacationTypeRepository vacationTypeRepository,
+                            ApplicationEventPublisher applicationEventPublisher,
+                            MessageSource messageSource) {
         this.vacationTypeRepository = vacationTypeRepository;
+        this.applicationEventPublisher = applicationEventPublisher;
         this.messageSource = messageSource;
     }
 
@@ -79,22 +84,29 @@ public class VacationTypeServiceImpl implements VacationTypeService {
             .map(vacationTypeEntity -> convert(vacationTypeEntity, messageSource))
             .map(vacationType -> {
                 final VacationTypeUpdate vacationTypeUpdate = byId.get(vacationType.getId());
-                if (vacationType instanceof ProvidedVacationType providedVacationType) {
-                    return Optional.of(updateProvidedVacationType(providedVacationType, vacationTypeUpdate));
-                } else if (vacationType instanceof CustomVacationType customVacationType) {
-                    return Optional.of(updateCustomVacationType(customVacationType, vacationTypeUpdate));
-                } else {
-                    LOG.error("cannot handle vacationTypeUpdate={} for unknown vacationType implementation.", vacationTypeUpdate);
-                    return Optional.empty();
+                switch (vacationType) {
+                    case ProvidedVacationType providedVacationType -> {
+                        return Optional.of(updateProvidedVacationType(providedVacationType, vacationTypeUpdate));
+                    }
+                    case CustomVacationType customVacationType -> {
+                        return Optional.of(updateCustomVacationType(customVacationType, vacationTypeUpdate));
+                    }
+                    default -> {
+                        LOG.error("cannot handle vacationTypeUpdate={} for unknown vacationType implementation.", vacationTypeUpdate);
+                        return Optional.empty();
+                    }
                 }
             })
             .filter(Optional::isPresent)
             .map(Optional::get)
             .map(VacationType.class::cast)
             .map(VacationTypeServiceImpl::convert)
-            .collect(toList());
+            .toList();
 
-        vacationTypeRepository.saveAll(updatedEntities);
+        vacationTypeRepository.saveAll(updatedEntities).stream()
+            .map(entity -> convert(entity, messageSource))
+            .map(VacationTypeUpdatedEvent::of)
+            .forEach(applicationEventPublisher::publishEvent);
     }
 
     @Override
@@ -111,20 +123,21 @@ public class VacationTypeServiceImpl implements VacationTypeService {
             })
             .toList();
 
-        vacationTypeRepository.saveAll(newEntities);
+        vacationTypeRepository.saveAll(newEntities).stream()
+            .map(entity -> convert(entity, messageSource))
+            .map(VacationTypeCreatedEvent::of)
+            .forEach(applicationEventPublisher::publishEvent);
     }
 
     public static VacationTypeEntity convert(VacationType<?> vacationType) {
-        if (vacationType instanceof ProvidedVacationType providedVacationType) {
-            return convertProvidedVacationType(providedVacationType);
-        } else if (vacationType instanceof CustomVacationType customVacationType) {
-            return convertCustomVacationType(customVacationType);
-        } else {
-            throw new IllegalStateException("could not convert unknown vacationType.");
-        }
+        return switch (vacationType) {
+            case ProvidedVacationType providedVacationType -> convertProvidedVacationType(providedVacationType);
+            case CustomVacationType customVacationType -> convertCustomVacationType(customVacationType);
+            default -> throw new IllegalStateException("could not convert unknown vacationType.");
+        };
     }
 
-    public static VacationType<? extends VacationType<?>> convert(VacationTypeEntity vacationTypeEntity, MessageSource messageSource) {
+    public static VacationType<?> convert(VacationTypeEntity vacationTypeEntity, MessageSource messageSource) {
         if (vacationTypeEntity.isCustom()) {
             return convertCustomVacationType(vacationTypeEntity, messageSource);
         } else {
@@ -159,7 +172,7 @@ public class VacationTypeServiceImpl implements VacationTypeService {
         }
     }
 
-    private static VacationTypeEntity createVacationTypeEntity( boolean active, VacationCategory category, String messageKey, boolean requiresApprovalToApply, boolean requiresApprovalToCancel, VacationTypeColor color, boolean visibleToEveryone) {
+    private static VacationTypeEntity createVacationTypeEntity(boolean active, VacationCategory category, String messageKey, boolean requiresApprovalToApply, boolean requiresApprovalToCancel, VacationTypeColor color, boolean visibleToEveryone) {
         final VacationTypeEntity vacationTypeEntity = new VacationTypeEntity();
         vacationTypeEntity.setCustom(false);
         vacationTypeEntity.setActive(active);
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeUpdatedEvent.java b/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeUpdatedEvent.java
new file mode 100644
index 000000000..26e02f36f
--- /dev/null
+++ b/src/main/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeUpdatedEvent.java
@@ -0,0 +1,11 @@
+package org.synyx.urlaubsverwaltung.application.vacationtype;
+
+import java.time.Instant;
+import java.util.UUID;
+
+public record VacationTypeUpdatedEvent(UUID id, Instant createdAt, VacationType<?> updatedVacationType) {
+
+    public static VacationTypeUpdatedEvent of(VacationType<?> updatedVacationType) {
+        return new VacationTypeUpdatedEvent(UUID.randomUUID(), Instant.now(), updatedVacationType);
+    }
+}
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventHandlerExtension.java b/src/main/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventHandlerExtension.java
new file mode 100644
index 000000000..7efffd211
--- /dev/null
+++ b/src/main/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventHandlerExtension.java
@@ -0,0 +1,91 @@
+package org.synyx.urlaubsverwaltung.extension.vacationtype;
+
+import de.focus_shift.urlaubsverwaltung.extension.api.tenancy.TenantSupplier;
+import de.focus_shift.urlaubsverwaltung.extension.api.vacationtype.VacationTypeUpdatedEventDTO;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.synyx.urlaubsverwaltung.application.vacationtype.CustomVacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.ProvidedVacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeCreatedEvent;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeLabel;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeUpdatedEvent;
+import org.synyx.urlaubsverwaltung.settings.SupportedLanguages;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+
+@Component
+@ConditionalOnProperty(value = "uv.extensions.enabled", havingValue = "true")
+class VacationTypeEventHandlerExtension {
+
+    private final TenantSupplier tenantSupplier;
+    private final ApplicationEventPublisher applicationEventPublisher;
+
+
+    VacationTypeEventHandlerExtension(TenantSupplier tenantSupplier,
+                                      ApplicationEventPublisher applicationEventPublisher) {
+        this.tenantSupplier = tenantSupplier;
+        this.applicationEventPublisher = applicationEventPublisher;
+    }
+
+    @EventListener
+    @Async
+    void onVacationTypeCreated(VacationTypeCreatedEvent event) {
+        publishVacationType(event.vacationType());
+    }
+
+    @EventListener
+    @Async
+    void onVacationTypeUpdated(VacationTypeUpdatedEvent event) {
+        publishVacationType(event.updatedVacationType());
+    }
+
+    private void publishVacationType(VacationType<?> vacationType) {
+        final String tenant = tenantSupplier.get();
+        final VacationTypeUpdatedEventDTO updatedEventDTO = toVacationTypeDTO(tenant, vacationType);
+        applicationEventPublisher.publishEvent(updatedEventDTO);
+    }
+
+    private VacationTypeUpdatedEventDTO toVacationTypeDTO(String tenantId, VacationType<?> vacationType) {
+        return VacationTypeUpdatedEventDTO.builder()
+            .id(UUID.randomUUID())
+            .tenantId(tenantId)
+            .sourceId(vacationType.getId())
+            .category(vacationType.getCategory().name())
+            .requiresApprovalToApply(vacationType.isRequiresApprovalToApply())
+            .requiresApprovalToCancel(vacationType.isRequiresApprovalToCancel())
+            .color(vacationType.getColor().name())
+            .visibleToEveryone(vacationType.isVisibleToEveryone())
+            .label(toLabels(vacationType))
+            .build();
+    }
+
+    private Map<Locale, String> toLabels(VacationType<?> vacationType) {
+        return switch (vacationType) {
+            case ProvidedVacationType providedVacationType -> toLabels(providedVacationType);
+            case CustomVacationType customVacationType -> toLabels(customVacationType);
+            default -> throw new IllegalArgumentException("Unsupported vacation type: " + vacationType);
+        };
+    }
+
+    private Map<Locale, String> toLabels(ProvidedVacationType providedVacationType) {
+        return Arrays.stream(SupportedLanguages.values())
+            .map(SupportedLanguages::getLocale)
+            .collect(toMap(identity(), providedVacationType::getLabel));
+    }
+
+    private Map<Locale, String> toLabels(CustomVacationType customVacationType) {
+        return customVacationType.labelsByLocale().values()
+            .stream()
+            .collect(toMap(VacationTypeLabel::locale, VacationTypeLabel::label));
+    }
+}
diff --git a/src/main/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventRepublisher.java b/src/main/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventRepublisher.java
new file mode 100644
index 000000000..7d6802ac9
--- /dev/null
+++ b/src/main/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventRepublisher.java
@@ -0,0 +1,51 @@
+package org.synyx.urlaubsverwaltung.extension.vacationtype;
+
+import org.slf4j.Logger;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.event.ApplicationStartedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeService;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeUpdatedEvent;
+
+import static java.lang.invoke.MethodHandles.lookup;
+import static org.slf4j.LoggerFactory.getLogger;
+
+@Component
+@ConditionalOnProperty(value = "uv.extensions.vacationtype.republish.enabled", havingValue = "true")
+@ConditionalOnBean(VacationTypeEventHandlerExtension.class)
+class VacationTypeEventRepublisher {
+
+    private static final Logger LOG = getLogger(lookup().lookupClass());
+
+    private final VacationTypeService vacationTypeService;
+    private final VacationTypeEventHandlerExtension vacationTypeEventHandlerExtension;
+
+    VacationTypeEventRepublisher(VacationTypeService vacationTypeService,
+                                 VacationTypeEventHandlerExtension vacationTypeEventHandlerExtension) {
+        this.vacationTypeService = vacationTypeService;
+        this.vacationTypeEventHandlerExtension = vacationTypeEventHandlerExtension;
+    }
+
+    @Async
+    @EventListener(ApplicationStartedEvent.class)
+    void republishEvents() {
+
+        LOG.info("Republishing all events with type=VacationTypeUpdatedEvent");
+
+        vacationTypeService.getAllVacationTypes()
+            .stream()
+            .map(VacationTypeUpdatedEvent::of)
+            .forEach(event -> {
+                final VacationType<?> updatedVacationType = event.updatedVacationType();
+                LOG.info("Publishing vacationTypeUpdatedEvent with id={}, vacationTypeId={} for category={} with active={} and requiresApprovalToApply={}",
+                    event.id(), updatedVacationType.getId(), updatedVacationType.getCategory(), updatedVacationType.isActive(), updatedVacationType.isRequiresApprovalToApply());
+                vacationTypeEventHandlerExtension.onVacationTypeUpdated(event);
+            });
+
+        LOG.info("Republished all events with type=VacationTypeUpdatedEvent");
+    }
+}


Test patch:
diff --git a/src/test/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImplTest.java b/src/test/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImplTest.java
index e504c17d4..8ebc92264 100644
--- a/src/test/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImplTest.java
+++ b/src/test/java/org/synyx/urlaubsverwaltung/application/vacationtype/VacationTypeServiceImplTest.java
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.MessageSource;
 import org.springframework.data.domain.Sort;
 
@@ -20,6 +21,8 @@ import static java.util.Locale.ENGLISH;
 import static java.util.Locale.GERMAN;
 import static java.util.Locale.JAPANESE;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.synyx.urlaubsverwaltung.application.vacationtype.VacationCategory.HOLIDAY;
@@ -39,11 +42,13 @@ class VacationTypeServiceImplTest {
     @Mock
     private VacationTypeRepository vacationTypeRepository;
     @Mock
+    private ApplicationEventPublisher applicationEventPublisher;
+    @Mock
     private MessageSource messageSource;
 
     @BeforeEach
     void setUp() {
-        sut = new VacationTypeServiceImpl(vacationTypeRepository, messageSource);
+        sut = new VacationTypeServiceImpl(vacationTypeRepository, applicationEventPublisher, messageSource);
     }
 
     @Test
@@ -283,4 +288,65 @@ class VacationTypeServiceImplTest {
             assertThat(entity.getMessageKey()).isNull();
         });
     }
+
+    @Test
+    void ensureCreatePublishesEvent() {
+
+        final VacationTypeEntity firstEntity = new VacationTypeEntity();
+        firstEntity.setId(1L);
+        firstEntity.setActive(true);
+        firstEntity.setCategory(OTHER);
+        firstEntity.setRequiresApprovalToApply(true);
+        firstEntity.setRequiresApprovalToCancel(true);
+        firstEntity.setColor(YELLOW);
+        firstEntity.setVisibleToEveryone(true);
+        firstEntity.setLabelByLocale(Map.of(GERMAN, "jokertag", ENGLISH, "jokerday"));
+        firstEntity.setCustom(true);
+
+        final VacationTypeEntity secondEntity = new VacationTypeEntity();
+        secondEntity.setId(2L);
+        secondEntity.setActive(false);
+        secondEntity.setCategory(HOLIDAY);
+        secondEntity.setRequiresApprovalToApply(false);
+        secondEntity.setRequiresApprovalToCancel(false);
+        secondEntity.setColor(BLUE);
+        secondEntity.setVisibleToEveryone(false);
+        secondEntity.setLabelByLocale(Map.of(GERMAN, "familientag", ENGLISH, "family day"));
+        secondEntity.setCustom(true);
+
+        when(vacationTypeRepository.saveAll(anyList())).thenReturn(List.of(firstEntity, secondEntity));
+
+        sut.createVacationTypes(List.of(
+            CustomVacationType.builder(messageSource).labels(List.of()).build(),
+            CustomVacationType.builder(messageSource).labels(List.of()).build()
+        ));
+
+        final ArgumentCaptor<VacationTypeCreatedEvent> captor = ArgumentCaptor.forClass(VacationTypeCreatedEvent.class);
+        verify(applicationEventPublisher, times(2)).publishEvent(captor.capture());
+
+        final List<VacationTypeCreatedEvent> actualPublishedEvents = captor.getAllValues();
+        assertThat(actualPublishedEvents).hasSize(2);
+        assertThat(actualPublishedEvents.getFirst().vacationType()).satisfies(vacationType -> {
+            assertThat(vacationType.getId()).isEqualTo(1L);
+            assertThat(vacationType.active).isTrue();
+            assertThat(vacationType.getCategory()).isEqualTo(OTHER);
+            assertThat(vacationType.isRequiresApprovalToApply()).isTrue();
+            assertThat(vacationType.isRequiresApprovalToCancel()).isTrue();
+            assertThat(vacationType.getColor()).isEqualTo(YELLOW);
+            assertThat(vacationType.isVisibleToEveryone()).isTrue();
+            assertThat(vacationType.getLabel(GERMAN)).isEqualTo("jokertag");
+            assertThat(vacationType.getLabel(ENGLISH)).isEqualTo("jokerday");
+        });
+        assertThat(actualPublishedEvents.get(1).vacationType()).satisfies(vacationType -> {
+            assertThat(vacationType.getId()).isEqualTo(2L);
+            assertThat(vacationType.active).isFalse();
+            assertThat(vacationType.getCategory()).isEqualTo(HOLIDAY);
+            assertThat(vacationType.isRequiresApprovalToApply()).isFalse();
+            assertThat(vacationType.isRequiresApprovalToCancel()).isFalse();
+            assertThat(vacationType.getColor()).isEqualTo(BLUE);
+            assertThat(vacationType.isVisibleToEveryone()).isFalse();
+            assertThat(vacationType.getLabel(GERMAN)).isEqualTo("familientag");
+            assertThat(vacationType.getLabel(ENGLISH)).isEqualTo("family day");
+        });
+    }
 }
diff --git a/src/test/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventHandlerExtensionTest.java b/src/test/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventHandlerExtensionTest.java
new file mode 100644
index 000000000..e0663e06a
--- /dev/null
+++ b/src/test/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventHandlerExtensionTest.java
@@ -0,0 +1,178 @@
+package org.synyx.urlaubsverwaltung.extension.vacationtype;
+
+
+import de.focus_shift.urlaubsverwaltung.extension.api.tenancy.TenantSupplier;
+import de.focus_shift.urlaubsverwaltung.extension.api.vacationtype.VacationTypeUpdatedEventDTO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.support.StaticMessageSource;
+import org.synyx.urlaubsverwaltung.application.vacationtype.CustomVacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.ProvidedVacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationCategory;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeColor;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeLabel;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeUpdatedEvent;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static java.util.Locale.forLanguageTag;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.synyx.urlaubsverwaltung.application.vacationtype.VacationCategory.HOLIDAY;
+import static org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeColor.YELLOW;
+
+@ExtendWith(MockitoExtension.class)
+class VacationTypeEventHandlerExtensionTest {
+
+    @Mock
+    private TenantSupplier tenantSupplier;
+
+    @Mock
+    private ApplicationEventPublisher applicationEventPublisher;
+
+    @Captor
+    private ArgumentCaptor<VacationTypeUpdatedEventDTO> eventCaptor;
+
+    private VacationTypeEventHandlerExtension vacationTypeEventHandlerExtension;
+
+    @BeforeEach
+    void setUp() {
+        vacationTypeEventHandlerExtension = new VacationTypeEventHandlerExtension(tenantSupplier, applicationEventPublisher);
+    }
+
+    @Test
+    void handlesCreatedEvent() {
+
+        final String tenant = "default";
+        when(tenantSupplier.get()).thenReturn(tenant);
+
+        final CustomVacationType vacationType = CustomVacationType.builder(new StaticMessageSource())
+            .color(YELLOW)
+            .id(42L)
+            .active(true)
+            .requiresApprovalToApply(false)
+            .requiresApprovalToCancel(false)
+            .visibleToEveryone(true)
+            .labels(List.of(
+                new VacationTypeLabel(Locale.GERMAN, "vacation-type-DE"),
+                new VacationTypeLabel(Locale.ENGLISH, "vacation-type-EN")
+            ))
+            .category(HOLIDAY)
+            .build();
+
+        vacationTypeEventHandlerExtension.onVacationTypeUpdated(VacationTypeUpdatedEvent.of(vacationType));
+
+        verify(applicationEventPublisher).publishEvent(eventCaptor.capture());
+        final VacationTypeUpdatedEventDTO actualEvent = eventCaptor.getValue();
+
+        assertThat(actualEvent.getId()).isNotNull();
+        assertThat(actualEvent.getTenantId()).isEqualTo(tenant);
+        assertThat(actualEvent.getCategory()).isEqualTo("HOLIDAY");
+        assertThat(actualEvent.isRequiresApprovalToApply()).isFalse();
+        assertThat(actualEvent.isRequiresApprovalToCancel()).isFalse();
+        assertThat(actualEvent.getColor()).isEqualTo("YELLOW");
+        assertThat(actualEvent.isVisibleToEveryone()).isTrue();
+    }
+
+    @Test
+    void handlesUpdatedEventOfProvidedVacationType() {
+        String tenant = "default";
+        when(tenantSupplier.get()).thenReturn(tenant);
+
+        ProvidedVacationType providedVacationType = mock(ProvidedVacationType.class);
+        when(providedVacationType.getId()).thenReturn(42L);
+        when(providedVacationType.getCategory()).thenReturn(VacationCategory.HOLIDAY);
+        when(providedVacationType.isRequiresApprovalToApply()).thenReturn(true);
+        when(providedVacationType.isRequiresApprovalToCancel()).thenReturn(true);
+        when(providedVacationType.getColor()).thenReturn(VacationTypeColor.YELLOW);
+        when(providedVacationType.isVisibleToEveryone()).thenReturn(true);
+        when(providedVacationType.getLabel(Locale.GERMAN)).thenReturn("Urlaub");
+        when(providedVacationType.getLabel(Locale.ENGLISH)).thenReturn("Holiday");
+        when(providedVacationType.getLabel(forLanguageTag("de-AT"))).thenReturn("Urlaub");
+        when(providedVacationType.getLabel(forLanguageTag("el"))).thenReturn("Holiday");
+        VacationTypeUpdatedEvent event = VacationTypeUpdatedEvent.of(providedVacationType);
+
+        vacationTypeEventHandlerExtension.onVacationTypeUpdated(event);
+
+        verify(applicationEventPublisher).publishEvent(eventCaptor.capture());
+        VacationTypeUpdatedEventDTO capturedEvent = eventCaptor.getValue();
+        assertThat(capturedEvent.getId()).isNotNull();
+        assertThat(capturedEvent.getTenantId()).isEqualTo(tenant);
+        assertThat(capturedEvent.getSourceId()).isEqualTo(providedVacationType.getId());
+        assertThat(capturedEvent.getCategory()).isEqualTo(providedVacationType.getCategory().name());
+        assertThat(capturedEvent.isRequiresApprovalToApply()).isEqualTo(providedVacationType.isRequiresApprovalToApply());
+        assertThat(capturedEvent.isRequiresApprovalToCancel()).isEqualTo(providedVacationType.isRequiresApprovalToCancel());
+        assertThat(capturedEvent.getColor()).isEqualTo(providedVacationType.getColor().name());
+        assertThat(capturedEvent.isVisibleToEveryone()).isEqualTo(providedVacationType.isVisibleToEveryone());
+        assertThat(capturedEvent.getLabel()).containsEntry(Locale.GERMAN, "Urlaub");
+        assertThat(capturedEvent.getLabel()).containsEntry(Locale.ENGLISH, "Holiday");
+        assertThat(capturedEvent.getLabel()).containsEntry(forLanguageTag("de-AT"), "Urlaub");
+        assertThat(capturedEvent.getLabel()).containsEntry(forLanguageTag("el"), "Holiday");
+
+    }
+
+    @Test
+    void handlesUpdatedEventOfCustomVacationType() {
+        String tenant = "default";
+        when(tenantSupplier.get()).thenReturn(tenant);
+
+        CustomVacationType customVacationType = mock(CustomVacationType.class);
+
+        when(customVacationType.getId()).thenReturn(42L);
+        when(customVacationType.getCategory()).thenReturn(VacationCategory.UNPAIDLEAVE);
+        when(customVacationType.isRequiresApprovalToApply()).thenReturn(false);
+        when(customVacationType.isRequiresApprovalToCancel()).thenReturn(false);
+        when(customVacationType.getColor()).thenReturn(VacationTypeColor.YELLOW);
+        when(customVacationType.isVisibleToEveryone()).thenReturn(true);
+        when(customVacationType.labelsByLocale()).thenReturn(Map.of(Locale.GERMAN, new VacationTypeLabel(Locale.GERMAN, "Gammeln")));
+
+        VacationTypeUpdatedEvent event = VacationTypeUpdatedEvent.of(customVacationType);
+
+        vacationTypeEventHandlerExtension.onVacationTypeUpdated(event);
+
+        verify(applicationEventPublisher).publishEvent(eventCaptor.capture());
+        VacationTypeUpdatedEventDTO capturedEvent = eventCaptor.getValue();
+        assertThat(capturedEvent.getId()).isNotNull();
+        assertThat(capturedEvent.getTenantId()).isEqualTo(tenant);
+        assertThat(capturedEvent.getSourceId()).isEqualTo(customVacationType.getId());
+        assertThat(capturedEvent.getCategory()).isEqualTo(customVacationType.getCategory().name());
+        assertThat(capturedEvent.isRequiresApprovalToApply()).isEqualTo(customVacationType.isRequiresApprovalToApply());
+        assertThat(capturedEvent.isRequiresApprovalToCancel()).isEqualTo(customVacationType.isRequiresApprovalToCancel());
+        assertThat(capturedEvent.getColor()).isEqualTo(customVacationType.getColor().name());
+        assertThat(capturedEvent.isVisibleToEveryone()).isEqualTo(customVacationType.isVisibleToEveryone());
+        assertThat(capturedEvent.getLabel()).containsEntry(Locale.GERMAN, "Gammeln");
+
+    }
+
+    @Test
+    void detectsUnsupportedVacationType() {
+
+        when(tenantSupplier.get()).thenReturn("default");
+
+        VacationType<?> unsupportedVacationType = mock(VacationType.class);
+        when(unsupportedVacationType.getId()).thenReturn(42L);
+        when(unsupportedVacationType.getCategory()).thenReturn(VacationCategory.UNPAIDLEAVE);
+        when(unsupportedVacationType.isRequiresApprovalToApply()).thenReturn(false);
+        when(unsupportedVacationType.isRequiresApprovalToCancel()).thenReturn(false);
+        when(unsupportedVacationType.getColor()).thenReturn(VacationTypeColor.YELLOW);
+        when(unsupportedVacationType.isVisibleToEveryone()).thenReturn(true);
+
+        VacationTypeUpdatedEvent event = VacationTypeUpdatedEvent.of(unsupportedVacationType);
+
+        assertThatThrownBy(() -> vacationTypeEventHandlerExtension.onVacationTypeUpdated(event))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessageContaining("Unsupported vacation type: " + unsupportedVacationType);
+    }
+}
diff --git a/src/test/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventRepublisherTest.java b/src/test/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventRepublisherTest.java
new file mode 100644
index 000000000..b70ddd997
--- /dev/null
+++ b/src/test/java/org/synyx/urlaubsverwaltung/extension/vacationtype/VacationTypeEventRepublisherTest.java
@@ -0,0 +1,54 @@
+package org.synyx.urlaubsverwaltung.extension.vacationtype;
+
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationType;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeService;
+import org.synyx.urlaubsverwaltung.application.vacationtype.VacationTypeUpdatedEvent;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class VacationTypeEventRepublisherTest {
+
+    @Mock
+    private VacationTypeService vacationTypeService;
+
+    @Mock
+    private VacationTypeEventHandlerExtension vacationTypeEventHandlerExtension;
+
+    @Captor
+    private ArgumentCaptor<VacationTypeUpdatedEvent> eventCaptor;
+
+    private VacationTypeEventRepublisher vacationTypeEventRepublisher;
+
+    @BeforeEach
+    void setUp() {
+        vacationTypeEventRepublisher = new VacationTypeEventRepublisher(vacationTypeService, vacationTypeEventHandlerExtension);
+    }
+
+    @Test
+    void republishEvents() {
+        VacationType<?> vacationType = mock(VacationType.class);
+        when(vacationTypeService.getAllVacationTypes()).thenReturn(List.of(vacationType));
+
+        vacationTypeEventRepublisher.republishEvents();
+
+        verify(vacationTypeService).getAllVacationTypes();
+
+        verify(vacationTypeEventHandlerExtension).onVacationTypeUpdated(eventCaptor.capture());
+        VacationTypeUpdatedEvent capturedEvent = eventCaptor.getValue();
+        assertThat(capturedEvent.updatedVacationType()).isSameAs(vacationType);
+    }
+}