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 <[email protected]>
  • Loading branch information
mustaphazorgati and arolfes committed Aug 1, 2023
1 parent 61b8ba0 commit fbd388e
Show file tree
Hide file tree
Showing 21 changed files with 3,046 additions and 201 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit fbd388e

Please sign in to comment.