Skip to content

Commit

Permalink
consume absence-type updates (#428)
Browse files Browse the repository at this point in the history
closes #426 

Here are some things you should have thought about:

**Multi-Tenancy**
- [ ] Extended new entities with `AbstractTenantAwareEntity`?
- [ ] New entity added to `TenantAwareDatabaseConfiguration`?
- [x] Extended new entities with `AdminAware`?
- [x] New entity added to `AdminAwareDatabaseConfiguration`?
- [ ] Tested with `dev-multitenant` profile?

<!--

Thanks for contributing to the zeiterfassung.
Please review the following notes before submitting you pull request.

Please look for other issues or pull requests which already work on this
topic. Is somebody already on it? Do you need to synchronize?

# Security Vulnerabilities

🛑 STOP! 🛑 If your contribution fixes a security vulnerability, please do
not submit it.
Instead, please write an E-Mail to [email protected] with all the
information
to recreate the security vulnerability.

# Describing Your Changes

If, having reviewed the notes above, you're ready to submit your pull
request, please
provide a brief description of the proposed changes.

If they:
🐞 fix a bug, please describe the broken behaviour and how the changes
fix it.
    Please label with 'type: bug' and 'status: new'
    
🎁 make an enhancement, please describe the new functionality and why you
believe it's useful.
    Please label with 'type: enhancement' and 'status: new'
 
If your pull request relates to any existing issues,
please reference them by using the issue number prefixed with #.

-->
  • Loading branch information
honnel authored May 10, 2024
2 parents 2c1a524 + f9122ca commit 9883b74
Show file tree
Hide file tree
Showing 28 changed files with 912 additions and 100 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<scope>test</scope>
</dependency>

<!-- Database -->
<dependency>
Expand Down Expand Up @@ -189,6 +194,11 @@
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
Expand Down
31 changes: 29 additions & 2 deletions src/main/java/de/focusshift/zeiterfassung/absence/AbsenceType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* 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<Locale, String> 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.
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<Long> {

@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<Locale, String> 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() {
Expand All @@ -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<Locale, String> getLabelByLocale() {
return labelByLocale;
}

public void setLabelByLocale(Map<Locale, String> 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 +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.focusshift.zeiterfassung.absence;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

interface AbsenceTypeRepository extends JpaRepository<AbsenceTypeEntity, Long> {

Optional<AbsenceTypeEntity> findByTenantIdAndSourceId(String tenantId, Long sourceId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.focusshift.zeiterfassung.absence;

public interface AbsenceTypeService {

void updateAbsenceType(AbsenceTypeUpdate absenceTypeUpdate);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Locale, String> labelByLocale
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class AbsenceWriteEntity implements AdminAware<Long> {
private DayLength dayLength;

@Embedded
private AbsenceTypeEntity type;
private AbsenceTypeEntityEmbeddable type;

@Column(nullable = false)
@Enumerated(STRING)
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<Locale, String>, String> {

private static final Logger LOG = getLogger(lookup().lookupClass());

private static final ObjectMapper om = new ObjectMapper();

@Override
public String convertToDatabaseColumn(Map<Locale, String> attribute) {
try {
return om.writeValueAsString(attribute);
} catch (JsonProcessingException ex) {
LOG.error("could not write value as string", ex);
return null;
}
}

@Override
public Map<Locale, String> 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();
}
}
}
Loading

0 comments on commit 9883b74

Please sign in to comment.