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