diff --git a/pom.xml b/pom.xml index 97fb74493..5ab6fdafe 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,11 @@ org.springframework.boot spring-boot-starter-amqp + + org.testcontainers + rabbitmq + test + @@ -189,6 +194,11 @@ spring-rabbit-test test + + org.springframework.boot + spring-boot-testcontainers + test + org.testcontainers testcontainers diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceType.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceType.java index f2dbae292..f6fbedb45 100644 --- a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceType.java +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceType.java @@ -2,7 +2,34 @@ import org.springframework.lang.Nullable; -public record AbsenceType(AbsenceTypeCategory category, @Nullable Long sourceId) { +import java.util.Locale; +import java.util.Map; + +/** + * Describes the absence-type of an {@linkplain Absence}. Absence types are not managed by Zeiterfassung, but by an + * external system. + * + *

+ * Therefore, existing labels are unknown. There should be at least one, actually, but you have to handle {@code null} + * values yourself. Or {@linkplain Locale locales} that are not existing in {@linkplain #labelByLocale}. + * + * @param category {@linkplain AbsenceTypeCategory} of this {@linkplain AbsenceType} + * @param sourceId external id of this {@linkplain AbsenceType} + * @param labelByLocale label of this {@linkplain AbsenceType} for a given {@linkplain Locale} + */ +public record AbsenceType( + AbsenceTypeCategory category, + @Nullable Long sourceId, + @Nullable Map labelByLocale +) { + + public AbsenceType(AbsenceTypeCategory category) { + this(category, null, null); + } + + public AbsenceType(AbsenceTypeCategory category, @Nullable Long sourceId) { + this(category, sourceId, null); + } /** * implicitly known HOLIDAY absence to ease testing. @@ -16,5 +43,5 @@ public record AbsenceType(AbsenceTypeCategory category, @Nullable Long sourceId) */ public static AbsenceType SPECIALLEAVE = new AbsenceType(AbsenceTypeCategory.SPECIALLEAVE, 2000L); - public static AbsenceType SICK = new AbsenceType(AbsenceTypeCategory.SICK, null); + public static AbsenceType SICK = new AbsenceType(AbsenceTypeCategory.SICK); } diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntity.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntity.java index 373bd94f7..b5432563d 100644 --- a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntity.java +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntity.java @@ -1,28 +1,65 @@ package de.focusshift.zeiterfassung.absence; +import de.focusshift.zeiterfassung.tenancy.configuration.multi.AdminAware; import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.validation.constraints.Size; + +import java.util.Locale; +import java.util.Map; +import java.util.Objects; import static jakarta.persistence.EnumType.STRING; -@Embeddable -public class AbsenceTypeEntity { +@Entity(name = "absence_type") +public class AbsenceTypeEntity implements AdminAware { + + @Size(max = 255) + @Column(name = "tenant_id") + private String tenantId; - @Column(name = "type_category", nullable = false) + @Id + @Column(name = "id", unique = true, nullable = false, updatable = false) + @SequenceGenerator(name = "absence_type_seq", sequenceName = "absence_type_seq") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "absence_type_seq") + private Long id; + + @Column(name = "category", nullable = false) @Enumerated(STRING) private AbsenceTypeCategory category; - @Column(name = "type_source_id") + @Column(name = "source_id", nullable = false) private Long sourceId; - public AbsenceTypeEntity(AbsenceTypeCategory category, Long sourceId) { - this.category = category; - this.sourceId = sourceId; + @Column(name = "color", nullable = false) + @Enumerated(STRING) + private AbsenceColor color; + + @Column(name = "label_by_locale", nullable = false) + @Convert(converter = LabelByLocaleConverter.class) + private Map labelByLocale; + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; } - public AbsenceTypeEntity() { - // for @Embeddable + @Override + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; } public AbsenceTypeCategory getCategory() { @@ -40,4 +77,45 @@ public Long getSourceId() { public void setSourceId(Long sourceId) { this.sourceId = sourceId; } + + public AbsenceColor getColor() { + return color; + } + + public void setColor(AbsenceColor color) { + this.color = color; + } + + public Map getLabelByLocale() { + return labelByLocale; + } + + public void setLabelByLocale(Map labelByLocale) { + this.labelByLocale = labelByLocale; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbsenceTypeEntity that = (AbsenceTypeEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "AbsenceTypeEntity{" + + "tenantId='" + tenantId + '\'' + + ", id=" + id + + ", category=" + category + + ", sourceId=" + sourceId + + ", color=" + color + + ", labelByLocale=" + labelByLocale + + '}'; + } } diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntityEmbeddable.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntityEmbeddable.java new file mode 100644 index 000000000..0aef7734c --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeEntityEmbeddable.java @@ -0,0 +1,43 @@ +package de.focusshift.zeiterfassung.absence; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Enumerated; + +import static jakarta.persistence.EnumType.STRING; + +@Embeddable +public class AbsenceTypeEntityEmbeddable { + + @Column(name = "type_category", nullable = false) + @Enumerated(STRING) + private AbsenceTypeCategory category; + + @Column(name = "type_source_id") + private Long sourceId; + + public AbsenceTypeEntityEmbeddable(AbsenceTypeCategory category, Long sourceId) { + this.category = category; + this.sourceId = sourceId; + } + + public AbsenceTypeEntityEmbeddable() { + // for @Embeddable + } + + public AbsenceTypeCategory getCategory() { + return category; + } + + public void setCategory(AbsenceTypeCategory category) { + this.category = category; + } + + public Long getSourceId() { + return sourceId; + } + + public void setSourceId(Long sourceId) { + this.sourceId = sourceId; + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeRepository.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeRepository.java new file mode 100644 index 000000000..a6655bd49 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeRepository.java @@ -0,0 +1,10 @@ +package de.focusshift.zeiterfassung.absence; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +interface AbsenceTypeRepository extends JpaRepository { + + Optional findByTenantIdAndSourceId(String tenantId, Long sourceId); +} diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeService.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeService.java new file mode 100644 index 000000000..564582cd1 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeService.java @@ -0,0 +1,6 @@ +package de.focusshift.zeiterfassung.absence; + +public interface AbsenceTypeService { + + void updateAbsenceType(AbsenceTypeUpdate absenceTypeUpdate); +} diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeServiceImpl.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeServiceImpl.java new file mode 100644 index 000000000..86eae2a53 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeServiceImpl.java @@ -0,0 +1,33 @@ +package de.focusshift.zeiterfassung.absence; + +import org.springframework.stereotype.Service; + +import java.util.HashMap; + +@Service +class AbsenceTypeServiceImpl implements AbsenceTypeService { + + private final AbsenceTypeRepository repository; + + AbsenceTypeServiceImpl(AbsenceTypeRepository repository) { + this.repository = repository; + } + + @Override + public void updateAbsenceType(AbsenceTypeUpdate absenceTypeUpdate) { + + final String tenantId = absenceTypeUpdate.tenantId().tenantId(); + final Long sourceId = absenceTypeUpdate.sourceId(); + + final AbsenceTypeEntity entity = repository.findByTenantIdAndSourceId(tenantId, sourceId) + .orElseGet(AbsenceTypeEntity::new); + + entity.setTenantId(tenantId); + entity.setSourceId(sourceId); + entity.setCategory(absenceTypeUpdate.category()); + entity.setColor(absenceTypeUpdate.color()); + entity.setLabelByLocale(new HashMap<>(absenceTypeUpdate.labelByLocale())); + + repository.save(entity); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeUpdate.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeUpdate.java new file mode 100644 index 000000000..50824bb60 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceTypeUpdate.java @@ -0,0 +1,24 @@ +package de.focusshift.zeiterfassung.absence; + +import de.focusshift.zeiterfassung.tenancy.tenant.TenantId; + +import java.util.Locale; +import java.util.Map; + +/** + * Update {@linkplain AbsenceType} to the given values. + * + * @param tenantId {@linkplain TenantId} this {@linkplain AbsenceType} is linked to + * @param sourceId external system source identifier of the absence type + * @param category next {@linkplain AbsenceTypeCategory} + * @param color next {@linkplain AbsenceColor} + * @param labelByLocale next labels + */ +public record AbsenceTypeUpdate( + TenantId tenantId, + Long sourceId, + AbsenceTypeCategory category, + AbsenceColor color, + Map labelByLocale +) { +} diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteEntity.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteEntity.java index 58e7d0181..e50987241 100644 --- a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteEntity.java +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteEntity.java @@ -48,7 +48,7 @@ public class AbsenceWriteEntity implements AdminAware { private DayLength dayLength; @Embedded - private AbsenceTypeEntity type; + private AbsenceTypeEntityEmbeddable type; @Column(nullable = false) @Enumerated(STRING) @@ -110,11 +110,11 @@ public void setDayLength(DayLength dayLength) { this.dayLength = dayLength; } - public AbsenceTypeEntity getType() { + public AbsenceTypeEntityEmbeddable getType() { return type; } - public void setType(AbsenceTypeEntity type) { + public void setType(AbsenceTypeEntityEmbeddable type) { this.type = type; } diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImpl.java b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImpl.java index b9bb1085e..cda1e6c3f 100644 --- a/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImpl.java +++ b/src/main/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImpl.java @@ -92,8 +92,8 @@ private static void setEntityFields(AbsenceWriteEntity entity, AbsenceWrite abse entity.setColor(absence.color()); } - private static AbsenceTypeEntity setTypeEntityFields(AbsenceType absenceType) { - final AbsenceTypeEntity absenceTypeEntity = new AbsenceTypeEntity(); + private static AbsenceTypeEntityEmbeddable setTypeEntityFields(AbsenceType absenceType) { + final AbsenceTypeEntityEmbeddable absenceTypeEntity = new AbsenceTypeEntityEmbeddable(); absenceTypeEntity.setCategory(absenceType.category()); absenceTypeEntity.setSourceId(absenceType.sourceId()); return absenceTypeEntity; diff --git a/src/main/java/de/focusshift/zeiterfassung/absence/LabelByLocaleConverter.java b/src/main/java/de/focusshift/zeiterfassung/absence/LabelByLocaleConverter.java new file mode 100644 index 000000000..e4e561d31 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/absence/LabelByLocaleConverter.java @@ -0,0 +1,44 @@ +package de.focusshift.zeiterfassung.absence; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +import static java.lang.invoke.MethodHandles.lookup; +import static org.slf4j.LoggerFactory.getLogger; + +@Converter +public final class LabelByLocaleConverter implements AttributeConverter, String> { + + private static final Logger LOG = getLogger(lookup().lookupClass()); + + private static final ObjectMapper om = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(Map attribute) { + try { + return om.writeValueAsString(attribute); + } catch (JsonProcessingException ex) { + LOG.error("could not write value as string", ex); + return null; + } + } + + @Override + public Map convertToEntityAttribute(String dbData) { + try { + return om.readValue(dbData, new TypeReference<>() { + }); + } catch (IOException ex) { + LOG.error("could not convert to entity attribute", ex); + return Map.of(); + } + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/RabbitMessageConsumer.java b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/RabbitMessageConsumer.java new file mode 100644 index 000000000..c9302a697 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/RabbitMessageConsumer.java @@ -0,0 +1,34 @@ +package de.focusshift.zeiterfassung.integration.urlaubsverwaltung; + +import org.slf4j.Logger; + +import java.util.Optional; + +import static java.lang.invoke.MethodHandles.lookup; +import static org.slf4j.LoggerFactory.getLogger; + +public abstract class RabbitMessageConsumer { + + private static final Logger LOG = getLogger(lookup().lookupClass()); + + protected RabbitMessageConsumer() { + // + } + + /** + * Maps the source value to the given enum, returns empty Optional when the value cannot be parsed to the enum. + * + * @param source source object that should be mapped to an enum + * @param enumClass class of the enum + * @return the mapped enum value + * @param type of the enum + */ + protected static > Optional mapToEnum(String source, Class enumClass) { + try { + return Optional.of(Enum.valueOf(enumClass, source)); + } catch (IllegalArgumentException e) { + LOG.info("could not map source={} to enum={}", source, enumClass.getName()); + return Optional.empty(); + } + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/application/ApplicationEventHandlerRabbitmq.java b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/application/ApplicationEventHandlerRabbitmq.java index 75762199c..bdce4afe1 100644 --- a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/application/ApplicationEventHandlerRabbitmq.java +++ b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/application/ApplicationEventHandlerRabbitmq.java @@ -9,6 +9,7 @@ import de.focusshift.zeiterfassung.absence.AbsenceWrite; import de.focusshift.zeiterfassung.absence.AbsenceWriteService; import de.focusshift.zeiterfassung.absence.DayLength; +import de.focusshift.zeiterfassung.integration.urlaubsverwaltung.RabbitMessageConsumer; import de.focusshift.zeiterfassung.tenancy.tenant.TenantId; import de.focusshift.zeiterfassung.user.UserId; import org.slf4j.Logger; @@ -17,8 +18,6 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; import static de.focusshift.zeiterfassung.integration.urlaubsverwaltung.application.ApplicationRabbitmqConfiguration.ZEITERFASSUNG_URLAUBSVERWALTUNG_APPLICATION_ALLOWED_QUEUE; import static de.focusshift.zeiterfassung.integration.urlaubsverwaltung.application.ApplicationRabbitmqConfiguration.ZEITERFASSUNG_URLAUBSVERWALTUNG_APPLICATION_CANCELLED_QUEUE; @@ -26,7 +25,8 @@ import static java.lang.invoke.MethodHandles.lookup; import static org.slf4j.LoggerFactory.getLogger; -public class ApplicationEventHandlerRabbitmq { +public class ApplicationEventHandlerRabbitmq extends RabbitMessageConsumer { + private static final Logger LOG = getLogger(lookup().lookupClass()); private final AbsenceWriteService absenceWriteService; @@ -89,37 +89,15 @@ private static Optional toAbsence(ApplicationEventDtoAdapter event } private static Optional toDayLength(de.focus_shift.urlaubsverwaltung.extension.api.application.DayLength dayLength) { - return map(dayLength.name(), DayLength::valueOf) - .or(peek(() -> LOG.info("could not map dayLength"))); + return mapToEnum(dayLength.name(), DayLength.class); } private static Optional toAbsenceType(String absenceTypeCategoryName, Long sourceId) { - return map(absenceTypeCategoryName, AbsenceTypeCategory::valueOf) - .map(category -> new AbsenceType(category, sourceId)) - .or(peek(() -> LOG.info("could not map vacationTypeCategory to AbsenceType"))); + return mapToEnum(absenceTypeCategoryName, AbsenceTypeCategory.class) + .map(category -> new AbsenceType(category, sourceId)); } private static Optional toAbsenceColor(String vacationTypeColor) { - return map(vacationTypeColor, AbsenceColor::valueOf) - .or(peek(() -> LOG.info("could not map vacationTypeColor to AbsenceColor"))); - } - - private static Optional map(T t, Function mapper) { - try { - return Optional.of(mapper.apply(t)); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - } - - private static Supplier> peek(Runnable runnable) { - return () -> { - try { - runnable.run(); - } catch (Exception e) { - // - } - return Optional.empty(); - }; + return mapToEnum(vacationTypeColor, AbsenceColor.class); } } diff --git a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/sicknote/SickNoteEventHandlerRabbitmq.java b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/sicknote/SickNoteEventHandlerRabbitmq.java index 49bbccb2a..ecdaa6ba6 100644 --- a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/sicknote/SickNoteEventHandlerRabbitmq.java +++ b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/sicknote/SickNoteEventHandlerRabbitmq.java @@ -9,14 +9,13 @@ import de.focusshift.zeiterfassung.absence.AbsenceWrite; import de.focusshift.zeiterfassung.absence.AbsenceWriteService; import de.focusshift.zeiterfassung.absence.DayLength; +import de.focusshift.zeiterfassung.integration.urlaubsverwaltung.RabbitMessageConsumer; import de.focusshift.zeiterfassung.tenancy.tenant.TenantId; import de.focusshift.zeiterfassung.user.UserId; import org.slf4j.Logger; import org.springframework.amqp.rabbit.annotation.RabbitListener; import java.util.Optional; -import java.util.function.Function; -import java.util.function.Supplier; import static de.focusshift.zeiterfassung.integration.urlaubsverwaltung.sicknote.SickNoteRabbitmqConfiguration.ZEITERFASSUNG_URLAUBSVERWALTUNG_SICKNOTE_CANCELLED_QUEUE; import static de.focusshift.zeiterfassung.integration.urlaubsverwaltung.sicknote.SickNoteRabbitmqConfiguration.ZEITERFASSUNG_URLAUBSVERWALTUNG_SICKNOTE_CONVERTED_TO_APPLICATION_QUEUE; @@ -25,7 +24,7 @@ import static java.lang.invoke.MethodHandles.lookup; import static org.slf4j.LoggerFactory.getLogger; -public class SickNoteEventHandlerRabbitmq { +public class SickNoteEventHandlerRabbitmq extends RabbitMessageConsumer { private static final Logger LOG = getLogger(lookup().lookupClass()); private final AbsenceWriteService absenceWriteService; @@ -75,46 +74,20 @@ void on(SickNoteConvertedToApplicationEventDTO event) { } private static Optional toAbsence(SickNoteEventDtoAdapter event) { - - final Optional maybeDayLength = toDayLength(event.getPeriod().getDayLength()); - - if (maybeDayLength.isEmpty()) { - return Optional.empty(); - } - - return Optional.of(new AbsenceWrite( - new TenantId(event.getTenantId()), - event.getSourceId().longValue(), - new UserId(event.getPerson().getUsername()), - event.getPeriod().getStartDate(), - event.getPeriod().getEndDate(), - maybeDayLength.get(), - AbsenceType.SICK, - AbsenceColor.RED - )); + return toDayLength(event.getPeriod().getDayLength()) + .map(dayLength -> new AbsenceWrite( + new TenantId(event.getTenantId()), + event.getSourceId(), + new UserId(event.getPerson().getUsername()), + event.getPeriod().getStartDate(), + event.getPeriod().getEndDate(), + dayLength, + AbsenceType.SICK, + AbsenceColor.RED + )); } private static Optional toDayLength(de.focus_shift.urlaubsverwaltung.extension.api.sicknote.DayLength dayLength) { - return map(dayLength.name(), DayLength::valueOf) - .or(peek(() -> LOG.info("could not map dayLength"))); - } - - private static Optional map(T t, Function mapper) { - try { - return Optional.of(mapper.apply(t)); - } catch (IllegalArgumentException e) { - return Optional.empty(); - } - } - - private static Supplier> peek(Runnable runnable) { - return () -> { - try { - runnable.run(); - } catch (Exception e) { - // - } - return Optional.empty(); - }; + return mapToEnum(dayLength.name(), DayLength.class); } } diff --git a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeHandlerRabbitmq.java b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeHandlerRabbitmq.java new file mode 100644 index 000000000..e91b812ca --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeHandlerRabbitmq.java @@ -0,0 +1,64 @@ +package de.focusshift.zeiterfassung.integration.urlaubsverwaltung.vacationtype; + +import de.focus_shift.urlaubsverwaltung.extension.api.vacationtype.VacationTypeUpdatedEventDTO; +import de.focusshift.zeiterfassung.absence.AbsenceColor; +import de.focusshift.zeiterfassung.absence.AbsenceTypeCategory; +import de.focusshift.zeiterfassung.absence.AbsenceTypeService; +import de.focusshift.zeiterfassung.absence.AbsenceTypeUpdate; +import de.focusshift.zeiterfassung.integration.urlaubsverwaltung.RabbitMessageConsumer; +import de.focusshift.zeiterfassung.tenancy.tenant.TenantId; +import org.slf4j.Logger; +import org.springframework.amqp.rabbit.annotation.RabbitListener; + +import java.util.Optional; + +import static de.focusshift.zeiterfassung.integration.urlaubsverwaltung.vacationtype.VacationTypeRabbitmqConfiguration.ZEITERFASSUNG_URLAUBSVERWALTUNG_VACATIONTYPE_UPDATED_QUEUE; +import static java.lang.invoke.MethodHandles.lookup; +import static org.slf4j.LoggerFactory.getLogger; + +class VacationTypeHandlerRabbitmq extends RabbitMessageConsumer { + + private static final Logger LOG = getLogger(lookup().lookupClass()); + + private final AbsenceTypeService absenceTypeService; + + VacationTypeHandlerRabbitmq(AbsenceTypeService absenceTypeService) { + this.absenceTypeService = absenceTypeService; + } + + @RabbitListener(queues = ZEITERFASSUNG_URLAUBSVERWALTUNG_VACATIONTYPE_UPDATED_QUEUE) + void on(VacationTypeUpdatedEventDTO event) { + + LOG.info("Received VacationTypeUpdatedEvent id={} for tenantId={}", event.getId(), event.getTenantId()); + + toAbsenceTypeUpdate(event) + .ifPresentOrElse( + absenceTypeService::updateAbsenceType, + () -> LOG.info("could not map VacationTypeUpdatedEvent -> could not update AbsenceType") + ); + } + + private Optional toAbsenceTypeUpdate(VacationTypeUpdatedEventDTO eventDTO) { + return toAbsenceColor(eventDTO.getColor()) + .flatMap(color -> + toAbsenceTypeCategory(eventDTO.getCategory()) + .map(category -> + new AbsenceTypeUpdate( + new TenantId(eventDTO.getTenantId()), + eventDTO.getSourceId(), + category, + color, + eventDTO.getLabel() + ) + ) + ); + } + + private static Optional toAbsenceTypeCategory(String category) { + return mapToEnum(category, AbsenceTypeCategory.class); + } + + private static Optional toAbsenceColor(String color) { + return mapToEnum(color, AbsenceColor.class); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeRabbitmqConfiguration.java b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeRabbitmqConfiguration.java new file mode 100644 index 000000000..4ffdf80ad --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeRabbitmqConfiguration.java @@ -0,0 +1,52 @@ +package de.focusshift.zeiterfassung.integration.urlaubsverwaltung.vacationtype; + +import de.focusshift.zeiterfassung.absence.AbsenceTypeService; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(value = "zeiterfassung.integration.urlaubsverwaltung.vacationtype.enabled", havingValue = "true") +@EnableConfigurationProperties(VacationTypeRabbitmqConfigurationProperties.class) +class VacationTypeRabbitmqConfiguration { + + static final String ZEITERFASSUNG_URLAUBSVERWALTUNG_VACATIONTYPE_UPDATED_QUEUE = "zeiterfassung.queue.urlaubsverwaltung.vacationtype.updated"; + + @Bean + VacationTypeHandlerRabbitmq vacationTypeHandlerRabbitmq(AbsenceTypeService absenceTypeService) { + return new VacationTypeHandlerRabbitmq(absenceTypeService); + } + + @ConditionalOnProperty(value = "zeiterfassung.integration.urlaubsverwaltung.vacationtype.manage-topology", havingValue = "true") + static class ManageTopologyConfiguration { + + private final VacationTypeRabbitmqConfigurationProperties vacationTypeRabbitmqConfigurationProperties; + + ManageTopologyConfiguration(VacationTypeRabbitmqConfigurationProperties vacationTypeRabbitmqConfigurationProperties) { + this.vacationTypeRabbitmqConfigurationProperties = vacationTypeRabbitmqConfigurationProperties; + } + + @Bean + public TopicExchange vacationTypeTopic() { + return new TopicExchange(vacationTypeRabbitmqConfigurationProperties.getTopic()); + } + + @Bean + Queue zeiterfassungUrlaubsverwatlungVacationTypeUpdatedQueue() { + return new Queue(ZEITERFASSUNG_URLAUBSVERWALTUNG_VACATIONTYPE_UPDATED_QUEUE, true); + } + + @Bean + Binding bindZeiterfassungUrlaubsverwaltungVacationTypeUpdatedQueue() { + final String routingKeyUpdated = vacationTypeRabbitmqConfigurationProperties.getRoutingKeyUpdated(); + return BindingBuilder.bind(zeiterfassungUrlaubsverwatlungVacationTypeUpdatedQueue()) + .to(vacationTypeTopic()) + .with(routingKeyUpdated); + } + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeRabbitmqConfigurationProperties.java b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeRabbitmqConfigurationProperties.java new file mode 100644 index 000000000..79a0bc537 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeRabbitmqConfigurationProperties.java @@ -0,0 +1,52 @@ +package de.focusshift.zeiterfassung.integration.urlaubsverwaltung.vacationtype; + +import jakarta.validation.constraints.NotEmpty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@Validated +@ConfigurationProperties("zeiterfassung.integration.urlaubsverwaltung.vacationtype") +class VacationTypeRabbitmqConfigurationProperties { + + private boolean enabled = false; + + private boolean manageTopology = false; + + @NotEmpty + private String topic = "vacationtype.topic"; + + @NotEmpty + private String routingKeyUpdated = "updated"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isManageTopology() { + return manageTopology; + } + + public void setManageTopology(boolean manageTopology) { + this.manageTopology = manageTopology; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getRoutingKeyUpdated() { + return routingKeyUpdated; + } + + public void setRoutingKeyUpdated(String routingKeyUpdated) { + this.routingKeyUpdated = routingKeyUpdated; + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/tenancy/configuration/multi/AdminAwareDatabaseConfiguration.java b/src/main/java/de/focusshift/zeiterfassung/tenancy/configuration/multi/AdminAwareDatabaseConfiguration.java index 5b5c89531..31d2a9df3 100644 --- a/src/main/java/de/focusshift/zeiterfassung/tenancy/configuration/multi/AdminAwareDatabaseConfiguration.java +++ b/src/main/java/de/focusshift/zeiterfassung/tenancy/configuration/multi/AdminAwareDatabaseConfiguration.java @@ -1,6 +1,7 @@ package de.focusshift.zeiterfassung.tenancy.configuration.multi; import com.zaxxer.hikari.HikariDataSource; +import de.focusshift.zeiterfassung.absence.AbsenceTypeEntity; import de.focusshift.zeiterfassung.absence.AbsenceWriteEntity; import de.focusshift.zeiterfassung.security.oidc.clientregistration.OidcClientEntity; import de.focusshift.zeiterfassung.tenancy.tenant.TenantEntity; @@ -26,7 +27,7 @@ @Configuration(proxyBeanMethods = false) @EnableTransactionManagement @EnableJpaRepositories( - basePackageClasses = {TenantEntity.class, OidcClientEntity.class, AbsenceWriteEntity.class}, + basePackageClasses = {TenantEntity.class, OidcClientEntity.class, AbsenceWriteEntity.class, AbsenceTypeEntity.class}, entityManagerFactoryRef = "adminEntityManagerFactory", transactionManagerRef = "adminTransactionManager" ) @@ -56,7 +57,7 @@ LocalContainerEntityManagerFactoryBean adminEntityManagerFactory(EntityManagerFa return builder .dataSource(adminDataSource) // List all admin related entity packages here - .packages(TenantEntity.class, OidcClientEntity.class, AbsenceWriteEntity.class) + .packages(TenantEntity.class, OidcClientEntity.class, AbsenceWriteEntity.class, AbsenceTypeEntity.class) .persistenceUnit("admin") .build(); } diff --git a/src/main/resources/db/changelog/changelog-2.3.0-add-absence-type.xml b/src/main/resources/db/changelog/changelog-2.3.0-add-absence-type.xml new file mode 100644 index 000000000..a5333d8f5 --- /dev/null +++ b/src/main/resources/db/changelog/changelog-2.3.0-add-absence-type.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ALTER TABLE absence_type ENABLE ROW LEVEL SECURITY; + DROP POLICY IF EXISTS absence_type_tenant_isolation_policy ON absence_type; + CREATE POLICY absence_type_tenant_isolation_policy ON absence_type + USING (tenant_id = current_setting('app.tenant_id')::VARCHAR); + + + + diff --git a/src/main/resources/db/changelog/db.changelog-main.xml b/src/main/resources/db/changelog/db.changelog-main.xml index 4cd1f9d9e..2009fc50f 100644 --- a/src/main/resources/db/changelog/db.changelog-main.xml +++ b/src/main/resources/db/changelog/db.changelog-main.xml @@ -24,5 +24,6 @@ + diff --git a/src/test/java/de/focusshift/zeiterfassung/ArgumentsPermutation.java b/src/test/java/de/focusshift/zeiterfassung/ArgumentsPermutation.java new file mode 100644 index 000000000..1aa0f7734 --- /dev/null +++ b/src/test/java/de/focusshift/zeiterfassung/ArgumentsPermutation.java @@ -0,0 +1,29 @@ +package de.focusshift.zeiterfassung; + +import org.junit.jupiter.params.provider.Arguments; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +public final class ArgumentsPermutation { + + private ArgumentsPermutation() { + // + } + + public static Stream of(A[] a, B[] b) { + return Permutation.of(a, b).map(Arguments::of); + } + + static class Permutation { + + static Stream of(A[] a, B[] b) { + return of(Arrays.asList(a), Arrays.asList(b)); + } + + static Stream of(Collection firstList, Collection secondList) { + return firstList.stream().flatMap(a -> secondList.stream().map(b -> new Object[]{a, b})); + } + } +} diff --git a/src/test/java/de/focusshift/zeiterfassung/RabbitTestConfiguration.java b/src/test/java/de/focusshift/zeiterfassung/RabbitTestConfiguration.java new file mode 100644 index 000000000..947e601da --- /dev/null +++ b/src/test/java/de/focusshift/zeiterfassung/RabbitTestConfiguration.java @@ -0,0 +1,18 @@ +package de.focusshift.zeiterfassung; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.utility.DockerImageName; + +@TestConfiguration +public class RabbitTestConfiguration { + + @Bean + @ServiceConnection + public RabbitMQContainer rabbitMQContainer() { + return new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.7.25-management-alpine")) + .withReuse(true); + } +} diff --git a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceRepositoryIT.java b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceRepositoryIT.java index 590acee42..e17bf022e 100644 --- a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceRepositoryIT.java +++ b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceRepositoryIT.java @@ -57,7 +57,7 @@ private static AbsenceWriteEntity absence(long sourceId, String start, String en oneDayBeforeRequestedWeek.setStartDate(Instant.parse(start)); oneDayBeforeRequestedWeek.setEndDate(Instant.parse(end)); oneDayBeforeRequestedWeek.setDayLength(DayLength.FULL); - oneDayBeforeRequestedWeek.setType(new AbsenceTypeEntity(AbsenceTypeCategory.HOLIDAY, 1000L)); + oneDayBeforeRequestedWeek.setType(new AbsenceTypeEntityEmbeddable(AbsenceTypeCategory.HOLIDAY, 1000L)); oneDayBeforeRequestedWeek.setColor(AbsenceColor.PINK); return oneDayBeforeRequestedWeek; } diff --git a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceServiceImplTest.java b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceServiceImplTest.java index 5538a0b39..02773c76a 100644 --- a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceServiceImplTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceServiceImplTest.java @@ -72,7 +72,7 @@ void ensureFindAllAbsences() { entity_1.setStartDate(today.plusDays(1).toInstant()); entity_1.setEndDate(today.plusDays(2).toInstant()); entity_1.setDayLength(FULL); - entity_1.setType(new AbsenceTypeEntity(AbsenceType.HOLIDAY.category(), AbsenceType.HOLIDAY.sourceId())); + entity_1.setType(new AbsenceTypeEntityEmbeddable(AbsenceType.HOLIDAY.category(), AbsenceType.HOLIDAY.sourceId())); entity_1.setColor(AbsenceColor.PINK); final AbsenceWriteEntity entity_2 = new AbsenceWriteEntity(); @@ -81,7 +81,7 @@ void ensureFindAllAbsences() { entity_2.setStartDate(today.plusDays(4).toInstant()); entity_2.setEndDate(today.plusDays(4).toInstant()); entity_2.setDayLength(MORNING); - entity_2.setType(new AbsenceTypeEntity(AbsenceType.SPECIALLEAVE.category(), AbsenceType.SPECIALLEAVE.sourceId())); + entity_2.setType(new AbsenceTypeEntityEmbeddable(AbsenceType.SPECIALLEAVE.category(), AbsenceType.SPECIALLEAVE.sourceId())); entity_2.setColor(VIOLET); when(repository.findAllByTenantIdAndUserIdInAndStartDateLessThanAndEndDateGreaterThanEqual("tenant", List.of("user"), endDateExclusive, startDate)) @@ -177,7 +177,7 @@ void ensureGetAbsencesByUserIds() { absenceEntity_1.setStartDate(absence_1_start); absenceEntity_1.setEndDate(absence_1_end); absenceEntity_1.setDayLength(FULL); - absenceEntity_1.setType(new AbsenceTypeEntity(OTHER, 1000L)); + absenceEntity_1.setType(new AbsenceTypeEntityEmbeddable(OTHER, 1000L)); absenceEntity_1.setColor(YELLOW); final Instant absence_2_1_start = Instant.from(from.plusDays(1).atStartOfDay().atZone(berlin)); @@ -187,7 +187,7 @@ void ensureGetAbsencesByUserIds() { absenceEntity_2_1.setStartDate(absence_2_1_start); absenceEntity_2_1.setEndDate(absence_2_1_end); absenceEntity_2_1.setDayLength(MORNING); - absenceEntity_2_1.setType(new AbsenceTypeEntity(OTHER, 2000L)); + absenceEntity_2_1.setType(new AbsenceTypeEntityEmbeddable(OTHER, 2000L)); absenceEntity_2_1.setColor(VIOLET); final Instant absence_2_2_start = Instant.from(from.plusDays(2).atStartOfDay().atZone(berlin)); @@ -197,7 +197,7 @@ void ensureGetAbsencesByUserIds() { absenceEntity_2_2.setStartDate(absence_2_2_start); absenceEntity_2_2.setEndDate(absence_2_2_end); absenceEntity_2_2.setDayLength(NOON); - absenceEntity_2_2.setType(new AbsenceTypeEntity(OTHER, 3000L)); + absenceEntity_2_2.setType(new AbsenceTypeEntityEmbeddable(OTHER, 3000L)); absenceEntity_2_2.setColor(CYAN); when(repository.findAllByTenantIdAndUserIdInAndStartDateLessThanAndEndDateGreaterThanEqual("tenant", List.of(userId_1.value(), userId_2.value()), toExclusiveStartOfDay, fromStartOfDay)) @@ -272,7 +272,7 @@ void ensureGetAbsencesForAllUsers() { absenceEntity_1.setStartDate(absence_1_start); absenceEntity_1.setEndDate(absence_1_end); absenceEntity_1.setDayLength(FULL); - absenceEntity_1.setType(new AbsenceTypeEntity(OTHER, 1000L)); + absenceEntity_1.setType(new AbsenceTypeEntityEmbeddable(OTHER, 1000L)); absenceEntity_1.setColor(YELLOW); final Instant absence_2_1_start = Instant.from(from.plusDays(1).atStartOfDay().atZone(berlin)); @@ -282,7 +282,7 @@ void ensureGetAbsencesForAllUsers() { absenceEntity_2_1.setStartDate(absence_2_1_start); absenceEntity_2_1.setEndDate(absence_2_1_end); absenceEntity_2_1.setDayLength(MORNING); - absenceEntity_2_1.setType(new AbsenceTypeEntity(OTHER, 2000L)); + absenceEntity_2_1.setType(new AbsenceTypeEntityEmbeddable(OTHER, 2000L)); absenceEntity_2_1.setColor(VIOLET); final Instant absence_2_2_start = Instant.from(from.plusDays(2).atStartOfDay().atZone(berlin)); @@ -292,7 +292,7 @@ void ensureGetAbsencesForAllUsers() { absenceEntity_2_2.setStartDate(absence_2_2_start); absenceEntity_2_2.setEndDate(absence_2_2_end); absenceEntity_2_2.setDayLength(NOON); - absenceEntity_2_2.setType(new AbsenceTypeEntity(OTHER, 3000L)); + absenceEntity_2_2.setType(new AbsenceTypeEntityEmbeddable(OTHER, 3000L)); absenceEntity_2_2.setColor(CYAN); when(repository.findAllByTenantIdAndStartDateLessThanAndEndDateGreaterThanEqual("tenant", toExclusiveStartOfDay, fromStartOfDay)) diff --git a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceTypeIT.java b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceTypeIT.java new file mode 100644 index 000000000..7a2bc7a2b --- /dev/null +++ b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceTypeIT.java @@ -0,0 +1,79 @@ +package de.focusshift.zeiterfassung.absence; + +import de.focus_shift.urlaubsverwaltung.extension.api.vacationtype.VacationTypeUpdatedEventDTO; +import de.focusshift.zeiterfassung.RabbitTestConfiguration; +import de.focusshift.zeiterfassung.TestContainersBase; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +@SpringBootTest( + properties = { + "zeiterfassung.integration.urlaubsverwaltung.vacationtype.enabled=true", + "zeiterfassung.integration.urlaubsverwaltung.vacationtype.manage-topology=true", + "zeiterfassung.integration.urlaubsverwaltung.vacationtype.topic=vacationtype.topic", + "zeiterfassung.integration.urlaubsverwaltung.vacationtype.routing-key-updated=updated", + } +) +@Import(RabbitTestConfiguration.class) +@Transactional +class AbsenceTypeIT extends TestContainersBase { + + @Autowired + private AbsenceTypeService absenceTypeService; + + @Autowired + private AbsenceTypeRepository absenceTypeRepository; + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Test + void ensureAbsenceTypeCreation() { + + rabbitTemplate.convertAndSend("vacationtype.topic", "updated", VacationTypeUpdatedEventDTO.builder() + .id(UUID.randomUUID()) + .tenantId("tenant-id") + .sourceId(42L) + .category(AbsenceTypeCategory.OTHER.name()) + .requiresApprovalToApply(false) + .requiresApprovalToCancel(false) + .color(AbsenceColor.VIOLET.name()) + .visibleToEveryone(true) + .label(Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + )) + .build() + ); + + await().untilAsserted(() -> { + final List all = absenceTypeRepository.findAll(); + assertThat(all) + .hasSize(1) + .first() + .satisfies(entity -> { + assertThat(entity.getId()).isNotNull(); + assertThat(entity.getTenantId()).isEqualTo("tenant-id"); + assertThat(entity.getSourceId()).isEqualTo(42); + assertThat(entity.getCategory()).isEqualTo(AbsenceTypeCategory.OTHER); + assertThat(entity.getColor()).isEqualTo(AbsenceColor.VIOLET); + assertThat(entity.getLabelByLocale()).containsExactlyInAnyOrderEntriesOf(Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + )); + }); + }); + } +} diff --git a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceTypeServiceImplTest.java b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceTypeServiceImplTest.java new file mode 100644 index 000000000..f5c8846a2 --- /dev/null +++ b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceTypeServiceImplTest.java @@ -0,0 +1,97 @@ +package de.focusshift.zeiterfassung.absence; + +import de.focusshift.zeiterfassung.tenancy.tenant.TenantId; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AbsenceTypeServiceImplTest { + + @InjectMocks + private AbsenceTypeServiceImpl sut; + + @Mock + private AbsenceTypeRepository repository; + + @Test + void ensureCreatingNewAbsenceType() { + + when(repository.findByTenantIdAndSourceId("tenant", 42L)).thenReturn(Optional.empty()); + + final AbsenceTypeUpdate absenceTypeUpdate = new AbsenceTypeUpdate( + new TenantId("tenant"), + 42L, + AbsenceTypeCategory.OTHER, + AbsenceColor.VIOLET, + Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + ) + ); + + sut.updateAbsenceType(absenceTypeUpdate); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AbsenceTypeEntity.class); + verify(repository).save(captor.capture()); + + assertThat(captor.getValue()).satisfies(entity -> { + assertThat(entity.getId()).isNull(); + assertThat(entity.getTenantId()).isEqualTo("tenant"); + assertThat(entity.getCategory()).isEqualTo(AbsenceTypeCategory.OTHER); + assertThat(entity.getColor()).isEqualTo(AbsenceColor.VIOLET); + assertThat(entity.getLabelByLocale()).containsExactlyInAnyOrderEntriesOf(Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + )); + }); + } + + @Test + void ensureUpdateAbsenceType() { + + final AbsenceTypeEntity existingEntity = new AbsenceTypeEntity(); + existingEntity.setId(1L); + + when(repository.findByTenantIdAndSourceId("tenant", 42L)) + .thenReturn(Optional.of(existingEntity)); + + final AbsenceTypeUpdate absenceTypeUpdate = new AbsenceTypeUpdate( + new TenantId("tenant"), + 42L, + AbsenceTypeCategory.OTHER, + AbsenceColor.VIOLET, + Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + ) + ); + + sut.updateAbsenceType(absenceTypeUpdate); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(AbsenceTypeEntity.class); + verify(repository).save(captor.capture()); + + assertThat(captor.getValue()).satisfies(entity -> { + assertThat(entity.getId()).isEqualTo(1L); + assertThat(entity.getTenantId()).isEqualTo("tenant"); + assertThat(entity.getCategory()).isEqualTo(AbsenceTypeCategory.OTHER); + assertThat(entity.getColor()).isEqualTo(AbsenceColor.VIOLET); + assertThat(entity.getLabelByLocale()).containsExactlyInAnyOrderEntriesOf(Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + )); + }); + } +} diff --git a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImplIT.java b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImplIT.java index a673c7a17..820608f69 100644 --- a/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImplIT.java +++ b/src/test/java/de/focusshift/zeiterfassung/absence/AbsenceWriteServiceImplIT.java @@ -35,7 +35,7 @@ void ensureDeleteAbsence() { existingEntity.setStartDate(startDate); existingEntity.setEndDate(endDate); existingEntity.setDayLength(DayLength.FULL); - existingEntity.setType(new AbsenceTypeEntity(AbsenceType.HOLIDAY.category(), AbsenceType.HOLIDAY.sourceId())); + existingEntity.setType(new AbsenceTypeEntityEmbeddable(AbsenceType.HOLIDAY.category(), AbsenceType.HOLIDAY.sourceId())); existingEntity.setColor(AbsenceColor.PINK); repository.save(existingEntity); @@ -70,7 +70,7 @@ void ensureDeleteAbsenceDoesNotDeleteWhenTenantIdIsDifferent() { existingEntity.setStartDate(startDate); existingEntity.setEndDate(endDate); existingEntity.setDayLength(DayLength.FULL); - existingEntity.setType(new AbsenceTypeEntity(AbsenceType.HOLIDAY.category(), AbsenceType.HOLIDAY.sourceId())); + existingEntity.setType(new AbsenceTypeEntityEmbeddable(AbsenceType.HOLIDAY.category(), AbsenceType.HOLIDAY.sourceId())); existingEntity.setColor(AbsenceColor.PINK); repository.save(existingEntity); diff --git a/src/test/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeHandlerRabbitmqTest.java b/src/test/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeHandlerRabbitmqTest.java new file mode 100644 index 000000000..105117353 --- /dev/null +++ b/src/test/java/de/focusshift/zeiterfassung/integration/urlaubsverwaltung/vacationtype/VacationTypeHandlerRabbitmqTest.java @@ -0,0 +1,100 @@ +package de.focusshift.zeiterfassung.integration.urlaubsverwaltung.vacationtype; + +import de.focus_shift.urlaubsverwaltung.extension.api.vacationtype.VacationTypeUpdatedEventDTO; +import de.focusshift.zeiterfassung.ArgumentsPermutation; +import de.focusshift.zeiterfassung.absence.AbsenceColor; +import de.focusshift.zeiterfassung.absence.AbsenceTypeCategory; +import de.focusshift.zeiterfassung.absence.AbsenceTypeService; +import de.focusshift.zeiterfassung.absence.AbsenceTypeUpdate; +import de.focusshift.zeiterfassung.tenancy.tenant.TenantId; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class VacationTypeHandlerRabbitmqTest { + + @InjectMocks + private VacationTypeHandlerRabbitmq sut; + + @Mock + private AbsenceTypeService absenceTypeService; + + @Test + void ensureAbsenceTypeUpdateIsIgnoredWhenCategoryCannotBeMapped() { + + final VacationTypeUpdatedEventDTO eventDto = VacationTypeUpdatedEventDTO.builder() + .id(UUID.randomUUID()) + .tenantId("tenant") + .sourceId(42L) + .category("unknown") + .color("VIOLET") + .label(Map.of()) + .build(); + + sut.on(eventDto); + verifyNoInteractions(absenceTypeService); + } + + @Test + void ensureAbsenceTypeUpdateIsIgnoredWhenColorCannotBeMapped() { + + final VacationTypeUpdatedEventDTO eventDto = VacationTypeUpdatedEventDTO.builder() + .id(UUID.randomUUID()) + .tenantId("tenant") + .sourceId(42L) + .category("OTHER") + .color("unknown") + .label(Map.of()) + .build(); + + sut.on(eventDto); + verifyNoInteractions(absenceTypeService); + } + + static Stream categoryColorPermutation() { + return ArgumentsPermutation.of(AbsenceTypeCategory.values(), AbsenceColor.values()); + } + + @ParameterizedTest + @MethodSource("categoryColorPermutation") + void ensureAbsenceTypeUpdateImpl(AbsenceTypeCategory category, AbsenceColor color) { + + final Map labels = Map.of( + Locale.GERMAN, "label-de", + Locale.ENGLISH, "label-en" + ); + + final VacationTypeUpdatedEventDTO eventDto = VacationTypeUpdatedEventDTO.builder() + .id(UUID.randomUUID()) + .tenantId("tenant") + .sourceId(42L) + .category(category.name()) + .color(color.name()) + .label(labels) + .build(); + + sut.on(eventDto); + + verify(absenceTypeService).updateAbsenceType(new AbsenceTypeUpdate( + new TenantId("tenant"), + 42L, + category, + color, + labels + )); + } +}