diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/QuarterBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/QuarterBusinessService.java index 8b21383a35..3e58cb46a7 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/QuarterBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/QuarterBusinessService.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,8 +82,7 @@ private int getStartOfBusinessYear(YearMonth startOfQuarter, int quarter) { return startOfQuarter.minusMonths((quarter - 1) * 3L).getYear(); } - private void generateQuarter(LocalDate start, String label, String schema) { - TenantContext.setCurrentTenant(schema); + private Quarter generateQuarter(LocalDate start, String label) { YearMonth yearMonth = YearMonth.from(start); Quarter quarter = Quarter.Builder @@ -94,14 +92,7 @@ private void generateQuarter(LocalDate start, String label, String schema) { .withEndDate(yearMonth.plusMonths(2).atEndOfMonth()) .build(); validator.validateOnGeneration(quarter); - quarterPersistenceService.save(quarter); - } - - private boolean isInLastMonthOfQuarter(int currentQuarter, int nextQuarter) { - // If the quarter 4 months in the future and the current are exactly 2 apart, - // we are in the final month of the current quarter. This works for all 4 cases: - // 1 -> 3 | 2 -> 4 | 3 -> 1 | 4 -> 2 - return Math.abs(nextQuarter - currentQuarter) == 2; + return quarterPersistenceService.save(quarter); } public YearMonth getCurrentYearMonth() { @@ -119,27 +110,42 @@ Map generateQuarters() { return quarters; } - @Scheduled(cron = "0 59 23 L * ?") // Cron expression for 23:59:00 on the last day of every month - public void scheduledGenerationQuarters() { - Map quarters = generateQuarters(); - YearMonth currentYearMonth = getCurrentYearMonth(); - YearMonth nextQuarterYearMonth = currentYearMonth.plusMonths(4); + private Quarter getOrCreateCurrentQuarter() { + Quarter current = getCurrentQuarter(); + if (current == null) { + current = createQuarter(getCurrentYearMonth()); + } + return current; + } - int currentQuarter = quarters.get(currentYearMonth.getMonthValue()); - int nextQuarter = quarters.get(nextQuarterYearMonth.getMonthValue()); + private Quarter createQuarter(YearMonth creationDate) { + String label = createQuarterLabel(creationDate, generateQuarters().get(creationDate.getMonthValue())); + return generateQuarter(creationDate.atDay(1), label); + } + + private boolean isFirstMonthOfQuarter(Quarter currentQuarter) { + YearMonth firstMonth = YearMonth.from(currentQuarter.getStartDate()); + return getCurrentYearMonth().equals(firstMonth); + } + @Scheduled(cron = "0 1 0 1 * ?") // Runs at 00:01 on the 1st of each month + public void scheduledGenerationQuarters() { + logger.warn("Start scheduled generation of quarters"); String initialTenant = TenantContext.getCurrentTenant(); - Set tenantSchemas = this.tenantConfigProvider.getAllTenantIds(); - // If we are in the last month of a quarter, generate the next quarter - if (isInLastMonthOfQuarter(currentQuarter, nextQuarter)) { - for (String schema : tenantSchemas) { - logger.info("Generated quarters on last day of month for tenant {}", schema); - String label = createQuarterLabel(nextQuarterYearMonth, nextQuarter); - generateQuarter(nextQuarterYearMonth.atDay(1), label, schema); + for (String schema : tenantConfigProvider.getAllTenantIds()) { + logger.warn("Start generating quarters on first day of month for tenant {}", schema); + TenantContext.setCurrentTenant(schema); + + Quarter currentQuarter = getOrCreateCurrentQuarter(); + if (isFirstMonthOfQuarter(currentQuarter)) { + YearMonth nextQuarter = YearMonth.from(currentQuarter.getEndDate()).plusMonths(1); + createQuarter(nextQuarter); } + logger.warn("Successfully generated quarters on first day of month for tenant {}", schema); } TenantContext.setCurrentTenant(initialTenant); + logger.warn("End scheduled generation of quarters"); } -} +} \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/QuarterBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/QuarterBusinessServiceTest.java index 1d3cd76cc3..4795aaa4eb 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/QuarterBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/QuarterBusinessServiceTest.java @@ -107,35 +107,64 @@ void shouldGetBacklogQuarter() { assertNull(quarterList.get(0).getEndDate()); } - @ParameterizedTest(name = "Should not generate a new quarter on scheduledGenerationQuarters() when it is not the last month of the quarter such as {0}") - @ValueSource(ints = { 1, 2, 4, 5, 7, 8, 10, 11 }) + @ParameterizedTest(name = "Should not generate a new quarter on scheduledGenerationQuarters() when it is not the first month of the quarter such as {0}") + @ValueSource(ints = { 2, 3, 5, 6, 8, 9, 11, 12 }) void shouldNotGenerateQuarterIfNotLastMonthOfQuarter(int month) { ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", 7); - Mockito.when(quarterBusinessService.getCurrentYearMonth()).thenReturn(YearMonth.of(2030, month)); quarterBusinessService.scheduledGenerationQuarters(); verify(quarterPersistenceService, never()).save(any()); } - @ParameterizedTest(name = "Should generate new quarter on scheduledGenerationQuarters() when it is the last month of the quarter such as {0}") - @ValueSource(ints = { 3, 6, 9, 12 }) - void shouldGenerateQuarterIfLastMonthOfQuarter(int month) { + @ParameterizedTest(name = "Should generate both new quarter if the current one does not exist on scheduledGenerationQuarters() when it is the first month of the quarter such as {0}") + @ValueSource(ints = { 1, 4, 7, 10 }) + void shouldGenerateBothQuartersIfLastMonthOfQuarterAndCurrentQuarterDoesNotExist(int month) { ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", 7); Mockito.doReturn(Set.of(TestHelper.SCHEMA_PITC)).when(tenantConfigProvider).getAllTenantIds(); Mockito.when(quarterBusinessService.getCurrentYearMonth()).thenReturn(YearMonth.of(2030, month)); + + LocalDate currentQuarterStart = LocalDate.of(2030, month, 1); + LocalDate currentQuarterEnd = currentQuarterStart.plusMonths(3).minusDays(1); + + Quarter currentQuarter = new Quarter(); + currentQuarter.setStartDate(currentQuarterStart); + currentQuarter.setEndDate(currentQuarterEnd); + + Mockito.when(quarterPersistenceService.save(currentQuarter)).thenReturn(currentQuarter); quarterBusinessService.scheduledGenerationQuarters(); + verify(quarterPersistenceService, times(2)).save(any()); + } + + @ParameterizedTest(name = "Should generate new quarter on scheduledGenerationQuarters() when it is the first month of the quarter such as {0}") + @ValueSource(ints = { 1, 4, 7, 10 }) + void shouldGenerateQuarterIfFirstMonthOfQuarter(int month) { + ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", 7); + Mockito.doReturn(Set.of(TestHelper.SCHEMA_PITC)).when(tenantConfigProvider).getAllTenantIds(); + + Mockito.when(quarterBusinessService.getCurrentYearMonth()).thenReturn(YearMonth.of(2030, month)); + + LocalDate currentQuarterStart = LocalDate.of(2030, month, 1); + LocalDate currentQuarterEnd = currentQuarterStart.plusMonths(3).minusDays(1); + + Quarter currentQuarter = new Quarter(); + currentQuarter.setStartDate(currentQuarterStart); + currentQuarter.setEndDate(currentQuarterEnd); + + Mockito.when(quarterBusinessService.getCurrentQuarter()).thenReturn(currentQuarter); + quarterBusinessService.scheduledGenerationQuarters(); + verify(quarterPersistenceService, times(1)).save(any()); } private static Stream generateQuarterParams() { return Stream - .of(Arguments.of(7, "GJ xx/yy-Qzz", YearMonth.of(2030, 3), "GJ 30/31-Q1"), - Arguments.of(7, "GJ xx/yy-Qzz", YearMonth.of(2030, 9), "GJ 30/31-Q3"), - Arguments.of(5, "GJ xx/yy-Qzz", YearMonth.of(2030, 4), "GJ 30/31-Q2"), - Arguments.of(1, "GJ xx-Qzz", YearMonth.of(2030, 9), "GJ 31-Q1"), - Arguments.of(1, "GJ xxxx-Qzz", YearMonth.of(2030, 6), "GJ 2030-Q4"), - Arguments.of(2, "xx-yy-xxxx-yyyy-Qzz", YearMonth.of(2030, 1), "30-31-2030-2031-Q2")); + .of(Arguments.of(7, "GJ xx/yy-Qzz", YearMonth.of(2030, 4), "GJ 30/31-Q1"), + Arguments.of(7, "GJ xx/yy-Qzz", YearMonth.of(2030, 10), "GJ 30/31-Q3"), + Arguments.of(5, "GJ xx/yy-Qzz", YearMonth.of(2030, 5), "GJ 30/31-Q2"), + Arguments.of(1, "GJ xx-Qzz", YearMonth.of(2030, 10), "GJ 31-Q1"), + Arguments.of(1, "GJ xxxx-Qzz", YearMonth.of(2030, 7), "GJ 2030-Q4"), + Arguments.of(2, "xx-yy-xxxx-yyyy-Qzz", YearMonth.of(2030, 2), "30-31-2030-2031-Q2")); } @ParameterizedTest(name = "Should generate quarters correctly on scheduledGenerationQuarters() with quarter start {0}, format {1}, current month of year {2} and label {3}") @@ -147,10 +176,10 @@ void shouldGenerateCorrectQuarter(int quarterStart, String quarterFormat, YearMo ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", quarterStart); ReflectionTestUtils.setField(quarterBusinessService, "quarterFormat", quarterFormat); - int monthsToNextQuarterStart = 4; + int monthsToNextQuarterStart = 3; LocalDate expectedStart = currentYearMonth.plusMonths(monthsToNextQuarterStart).atDay(1); - int monthsToNextQuarterEnd = 6; + int monthsToNextQuarterEnd = 5; LocalDate expectedEnd = currentYearMonth.plusMonths(monthsToNextQuarterEnd).atEndOfMonth(); Quarter expectedQuarter = Quarter.Builder @@ -161,7 +190,15 @@ void shouldGenerateCorrectQuarter(int quarterStart, String quarterFormat, YearMo .withEndDate(expectedEnd) .build(); + LocalDate currentQuarterStart = LocalDate.of(currentYearMonth.getYear(), currentYearMonth.getMonth(), 1); + LocalDate currentQuarterEnd = currentQuarterStart.plusMonths(3).minusDays(1); + + Quarter currentQuarter = new Quarter(); + currentQuarter.setStartDate(currentQuarterStart); + currentQuarter.setEndDate(currentQuarterEnd); + Mockito.when(quarterBusinessService.getCurrentYearMonth()).thenReturn(currentYearMonth); + Mockito.when(quarterPersistenceService.getCurrentQuarter()).thenReturn(currentQuarter); quarterBusinessService.scheduledGenerationQuarters(); @@ -207,7 +244,6 @@ void shouldGetQuartersBasedOnStart(int start, int month, int quarter) { @DisplayName("Should return null on scheduledGenerationQuarters() when no quarters need to be generated") @Test void shouldReturnNullWhenNoQuarterGenerationNeeded() { - Mockito.when(quarterBusinessService.getCurrentYearMonth()).thenReturn(YearMonth.of(2030, 4)); quarterBusinessService.scheduledGenerationQuarters(); verify(quarterPersistenceService, times(0)).save(any()); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java index 4fff04cb0d..f572b349e3 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java @@ -138,15 +138,30 @@ void getModelNameShouldReturnQuarter() { assertEquals(QUARTER, quarterPersistenceService.getModelName()); } - @ParameterizedTest(name = "Should generate quarter with Cron-Job when current month is the last month of the current quarter (Month: {0}, Quarter: {1})") - @CsvSource(value = { "1,1,0", "2,1,0", "3,1,1", "4,1,0", "5,1,0", "6,2,1", "7,1,0", "8,1,0", "9,3,1", "10,3,0", - "11,1,0", "12,4,1" }) - void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfInvocations) { + @ParameterizedTest(name = "Should generate quarter with Cron-Job when current month is the first month of the current quarter (Month: {0}, Quarter: {1})") + @CsvSource(value = { "1,4,1,1", "2,4,0,1", "3,4,0,1", "4,1,1,4", "5,1,0,4", "6,1,0,4", "7,2,1,7", "8,2,0,7", + "9,2,0,7", "10,3,1,10", "11,3,0,10", "12,3,0,10" }) + void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfInvocations, + int currentQuarterStart) { int startQuarter = 7; ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", startQuarter); int nextYear = Year.now().atMonth(startQuarter).plusMonths(month + 12 - 1).getYear(); int nextYearShort = nextYear % 1000; - String expectedLabel = "GJ " + nextYearShort + "/" + (nextYearShort + 1) + "-Q" + quarterIndex; + String expectedLabel; + if (quarterIndex == 4) { + expectedLabel = "GJ " + (nextYearShort - 1) + "/" + nextYearShort + "-Q" + quarterIndex; + } else { + expectedLabel = "GJ " + nextYearShort + "/" + (nextYearShort + 1) + "-Q" + quarterIndex; + } + + LocalDate start = LocalDate.of(nextYear, currentQuarterStart, 1); + LocalDate end = start.plusMonths(3).minusDays(1); + + Quarter currentQuarter = new Quarter(); + currentQuarter.setStartDate(start); + currentQuarter.setEndDate(end); + + Mockito.when(quarterBusinessService.getCurrentQuarter()).thenReturn(currentQuarter); Mockito.doReturn(YearMonth.of(nextYear, month)).when(quarterBusinessService).getCurrentYearMonth(); Mockito.doReturn(Set.of(TestHelper.SCHEMA_PITC)).when(tenantConfigProvider).getAllTenantIds();