Skip to content

Commit

Permalink
Closes #2347: Configuration to enable working day calculation instead…
Browse files Browse the repository at this point in the history
… of working time calculation

* added configuration property for detailed working time calculation
* added WorkingDayCalculatorImpl to replicate pre 6.0.0 behaviour
* bugfix ServiceLevelHandler comparing classificationKeys

---------

Co-authored-by: arolfes <arolfes@users.noreply.github.com>
  • Loading branch information
mustaphazorgati and arolfes committed Aug 1, 2023
1 parent 3336cbb commit 0ffa2f0
Show file tree
Hide file tree
Showing 21 changed files with 3,046 additions and 201 deletions.
Expand Up @@ -29,6 +29,7 @@ INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000021', 'ETI:0000000
INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000022', 'ETI:000000000000000000000000000000000022', '2018-01-29 15:55:22', null , null , '2018-01-29 15:55:22', null , '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000001' , 'GPK_KSC' , 'DOMAIN_A', 'PI_0000000000022' , 'DOC_0000000000000000022' , null , '00' , 'PASystem' , '00' , 'SDNR' , '11223344' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 );
INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000023', 'ETI:000000000000000000000000000000000023', '2018-01-29 15:55:23', null , null , '2018-01-29 15:55:23', null , '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000001' , 'GPK_KSC' , 'DOMAIN_A', 'PI_0000000000023' , 'DOC_0000000000000000023' , null , '00' , 'PASystem' , '00' , 'SDNR' , '11223344' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , 'lnp' , '' , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 );
INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000024', 'ETI:000000000000000000000000000000000024', '2018-01-29 15:55:24', null , null , '2018-01-29 15:55:24', null , '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000001' , 'GPK_KSC' , 'DOMAIN_A', 'PI_0000000000024' , 'DOC_0000000000000000024' , null , '00' , 'PASystem' , '00' , 'SDNR' , '11223344' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , '' , null , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 );
INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000201', 'ETI:000000000000000000000000000000000201', '2023-07-31 15:55:01', '2023-07-31 15:55:00', null , '2023-07-31 15:55:01', null , '2023-07-31 15:55:00', '2023-08-02 15:55:00', 'Task201' , 'creator_user_id' , 'Lorem ipsum was n Quatsch dolor sit amet.', 'Some custom Note' , 2 , -1 , 'READY' , 'EXTERN' , 'L110102' , 'CLI:100000000000000000000000000000000002', 'WBI:100000000000000000000000000000000006' , 'USER-1-1' , 'DOMAIN_A', 'BPI21' , 'PBPI21' , 'user-1-1' , 'MyCompany1', 'MySystem1', 'MyInstance1' , 'MyType1', 'MyValue1' , true , false , null , 'NONE' , null , 'pqr' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 );
-- TASK TABLE (ID , EXTERNAL_ID , CREATED , CLAIMED , COMPLETED , modified , received , planned , due , name , creator , description , note , priority, manual_priority, state , classification_category , classification_key, classification_id , workbasket_id , workbasket_key, domain , business_process_id, parent_business_process_id, owner , por_company , por_system , por_system_instance, por_type , por_value , is_read, is_transferred,callback_info , callback_state , custom_attributes ,custom1 ,custom2, ,custom3, ,custom4 ,custom5 ,custom6 ,custom7 ,custom8 ,custom9 ,custom10 ,custom11, ,custom12 ,custom13 ,custom14 ,custom15 ,custom16 , custom-int-1, custom-int-2, custom-int-3, custom-int-4, custom-int-5, custom-int-6, custom-int-7, custom-int-8
-- Tasks for WorkOnTaskAccTest
INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000025', 'ETI:000000000000000000000000000000000025', '2018-01-29 15:55:24', null , null , '2018-01-29 15:55:24', '2018-01-29 15:55:24', '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000007' , 'USER-1-2' , 'DOMAIN_A', 'PI_0000000000025' , 'DOC_0000000000000000025' , null , 'abcd00' , 'PASystem' , '00' , 'SDNR' , '98765432' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , '' , '' , 'ert' , 'ert' , 'ert' , 'ert' , 'abc' , 'ert' , 'ert' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 );
Expand Down
@@ -0,0 +1,118 @@
package pro.taskana.common.internal.workingtime;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.stream.LongStream;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.SystemException;

public class WorkingDayCalculatorImpl implements WorkingTimeCalculator {

private final ZoneId zoneId;
private final HolidaySchedule holidaySchedule;

public WorkingDayCalculatorImpl(HolidaySchedule holidaySchedule, ZoneId zoneId) {
this.holidaySchedule = holidaySchedule;
this.zoneId = zoneId;
}

@Override
public Instant subtractWorkingTime(Instant workStart, Duration workingTime)
throws InvalidArgumentException {
long days = convertWorkingDaysToDays(workStart, -workingTime.toDays(), ZeroDirection.SUB_DAYS);
return workStart.plus(Duration.ofDays(days));
}

@Override
public Instant addWorkingTime(Instant workStart, Duration workingTime)
throws InvalidArgumentException {
long days = convertWorkingDaysToDays(workStart, workingTime.toDays(), ZeroDirection.ADD_DAYS);
return workStart.plus(Duration.ofDays(days));
}

@Override
public Duration workingTimeBetween(Instant first, Instant second)
throws InvalidArgumentException {
long days = Duration.between(first, second).abs().toDays();
Instant firstInstant = first.isBefore(second) ? first : second;

long workingDaysBetween =
LongStream.range(1, days)
.mapToObj(day -> isWorkingDay(firstInstant.plus(day, ChronoUnit.DAYS)))
.filter(t -> t)
.count();
return Duration.ofDays(workingDaysBetween);
}

@Override
public boolean isWorkingDay(Instant instant) {
return !isWeekend(instant) && !isHoliday(instant);
}

@Override
public boolean isWeekend(Instant instant) {
DayOfWeek dayOfWeek = toDayOfWeek(instant);
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}

@Override
public boolean isHoliday(Instant instant) {
return holidaySchedule.isHoliday(toLocalDate(instant));
}

@Override
public boolean isGermanHoliday(Instant instant) {
return holidaySchedule.isGermanHoliday(toLocalDate(instant));
}

private long convertWorkingDaysToDays(
final Instant startTime, long numberOfDays, ZeroDirection zeroDirection) {
if (startTime == null) {
throw new SystemException(
"Internal Error: convertWorkingDaysToDays was called with a null startTime");
}
int direction = calculateDirection(numberOfDays, zeroDirection);
long limit = Math.abs(numberOfDays);
return LongStream.iterate(0, i -> i + direction)
.filter(day -> isWorkingDay(startTime.plus(day, ChronoUnit.DAYS)))
.skip(limit)
.findFirst()
.orElse(0);
}

private int calculateDirection(long numberOfDays, ZeroDirection zeroDirection) {
if (numberOfDays == 0) {
return zeroDirection.getDirection();
} else {
return numberOfDays >= 0 ? 1 : -1;
}
}

private LocalDate toLocalDate(Instant instant) {
return LocalDate.ofInstant(instant, zoneId);
}

private DayOfWeek toDayOfWeek(Instant instant) {
return toLocalDate(instant).getDayOfWeek();
}

private enum ZeroDirection {
SUB_DAYS(-1),
ADD_DAYS(1);

private final int direction;

ZeroDirection(int direction) {
this.direction = direction;
}

public int getDirection() {
return direction;
}
}
}
Expand Up @@ -62,6 +62,7 @@
import org.junit.platform.commons.support.AnnotationSupport;
import pro.taskana.TaskanaConfiguration;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.ErrorCode;
import pro.taskana.common.api.exceptions.TaskanaException;
import pro.taskana.common.api.exceptions.TaskanaRuntimeException;
Expand All @@ -71,7 +72,6 @@
import pro.taskana.common.internal.jobs.JobScheduler;
import pro.taskana.common.internal.logging.LoggingAspect;
import pro.taskana.common.internal.workingtime.HolidaySchedule;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.testapi.TaskanaIntegrationTest;

/**
Expand Down Expand Up @@ -437,7 +437,7 @@ void classesShouldNotUseWorkingDaysToDaysConverter() {
.that()
.areNotAssignableFrom(ArchitectureTest.class)
.and()
.areNotAssignableTo(WorkingTimeCalculatorImpl.class)
.areNotAssignableTo(WorkingTimeCalculator.class)
.and()
.areNotAssignableTo(TaskanaEngineImpl.class)
.and()
Expand Down
Expand Up @@ -251,6 +251,7 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled(
Map<String, List<String>> expectedClassificationCategories =
Map.of("TYPE_A", List.of("CATEGORY_A"), "TYPE_B", List.of("CATEGORY_B"));
// working time configuration
boolean expectedUseDetailedWorkingTimeCalculation = false;
Map<DayOfWeek, Set<LocalTimeInterval>> expectedWorkingTimeSchedule =
Map.of(DayOfWeek.MONDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.NOON)));
ZoneId expectedWorkingTimeScheduleTimeZone = ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4));
Expand Down Expand Up @@ -308,6 +309,7 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled(
.classificationTypes(expectedClassificationTypes)
.classificationCategoriesByType(expectedClassificationCategories)
// working time configuration
.useWorkingTimeCalculation(expectedUseDetailedWorkingTimeCalculation)
.workingTimeSchedule(expectedWorkingTimeSchedule)
.workingTimeScheduleTimeZone(expectedWorkingTimeScheduleTimeZone)
.customHolidays(expectedCustomHolidays)
Expand Down Expand Up @@ -368,6 +370,8 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled(
assertThat(configuration.getClassificationCategoriesByType())
.isEqualTo(expectedClassificationCategories);
// working time configuration
assertThat(configuration.isUseWorkingTimeCalculation())
.isEqualTo(expectedUseDetailedWorkingTimeCalculation);
assertThat(configuration.getWorkingTimeSchedule()).isEqualTo(expectedWorkingTimeSchedule);
assertThat(configuration.getWorkingTimeScheduleTimeZone())
.isEqualTo(expectedWorkingTimeScheduleTimeZone);
Expand Down Expand Up @@ -442,6 +446,7 @@ void should_PopulateEveryConfigurationProperty_When_UsingCopyConstructor() {
.classificationCategoriesByType(
Map.of("typeA", List.of("categoryA"), "typeB", List.of("categoryB")))
// working time configuration
.useWorkingTimeCalculation(false)
.workingTimeSchedule(
Map.of(
DayOfWeek.MONDAY,
Expand Down
Expand Up @@ -46,6 +46,7 @@
import pro.taskana.task.internal.models.TaskImpl;
import pro.taskana.testapi.TaskanaInject;
import pro.taskana.testapi.TaskanaIntegrationTest;
import pro.taskana.testapi.builder.TaskBuilder;
import pro.taskana.testapi.builder.WorkbasketAccessItemBuilder;
import pro.taskana.testapi.security.WithAccessId;
import pro.taskana.workbasket.api.WorkbasketPermission;
Expand Down Expand Up @@ -185,6 +186,50 @@ private List<String> createTasksWithExistingClassificationInAttachment(
@Nested
class UpdatePriorityAndServiceLevelTest {

@WithAccessId(user = "businessadmin")
@Test
void should_ChangeDueDate_When_ServiceLevelOfClassificationHasChanged() throws Exception {
Classification classification =
defaultTestClassification()
.priority(1)
.serviceLevel("P1D")
.buildAndStore(classificationService);
WorkbasketSummary workbasketSummary =
defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService);
WorkbasketAccessItemBuilder.newWorkbasketAccessItem()
.workbasketId(workbasketSummary.getId())
.accessId(currentUserContext.getUserid())
.permission(WorkbasketPermission.OPEN)
.permission(WorkbasketPermission.READ)
.permission(WorkbasketPermission.READTASKS)
.permission(WorkbasketPermission.APPEND)
.buildAndStore(workbasketService, "businessadmin");

Task task = new TaskBuilder()
.classificationSummary(classification.asSummary())
.workbasketSummary(workbasketSummary)
.primaryObjRef(defaultTestObjectReference().build())
.planned(Instant.parse("2021-04-27T15:34:00.000Z"))
.due(null)
.buildAndStore(taskService);

classificationService.updateClassification(classification);
runAssociatedJobs();
// read again the task from DB
task = taskService.getTask(task.getId());
assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P1D");
assertThat(task.getDue()).isAfterOrEqualTo("2021-04-28T15:33:59.999Z");

classification.setServiceLevel("P3D");
classificationService.updateClassification(classification);
runAssociatedJobs();

// read again the task from DB
task = taskService.getTask(task.getId());
assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P3D");
assertThat(task.getDue()).isEqualTo("2021-04-30T15:33:59.999Z");
}

@WithAccessId(user = "businessadmin")
@Test
void should_NotThrowException_When_UpdatingClassificationWithEmptyServiceLevel()
Expand Down

0 comments on commit 0ffa2f0

Please sign in to comment.