From 8699e68609f6c95e644ecf59441751cc5e32e569 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Tue, 5 Aug 2025 15:13:24 +0200 Subject: [PATCH 01/13] fix(cronjob): Generate new Quarter at the start of the month rather then the end #1556 --- .../business/QuarterBusinessService.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) 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..a186f51a9c 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 @@ -97,11 +97,12 @@ private void generateQuarter(LocalDate start, String label, String schema) { 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; + private boolean isInFirstMonthOfQuarter(int monthOfYear) { + // The first months of a quarter are 1, 4, 7, 10. + // In a modulo-3 calculation, these numbers all have a remainder of 1. + // (1 % 3 = 1, 4 % 3 = 1, 7 % 3 = 1, 10 % 3 = 1) + // All other months result in a remainder of 2 or 0. + return monthOfYear % 3 == 1; } public YearMonth getCurrentYearMonth() { @@ -119,23 +120,25 @@ Map generateQuarters() { return quarters; } - @Scheduled(cron = "0 59 23 L * ?") // Cron expression for 23:59:00 on the last day of every month + @Scheduled(cron = "0 1 0 1 * ?") // Cron expression for 00:01:00 on the first day of every month public void scheduledGenerationQuarters() { Map quarters = generateQuarters(); - YearMonth currentYearMonth = getCurrentYearMonth(); - YearMonth nextQuarterYearMonth = currentYearMonth.plusMonths(4); + YearMonth currentYearMonth = getCurrentYearMonth().minusMonths(1); int currentQuarter = quarters.get(currentYearMonth.getMonthValue()); - int nextQuarter = quarters.get(nextQuarterYearMonth.getMonthValue()); String initialTenant = TenantContext.getCurrentTenant(); + System.out.println("initialTenant = " + initialTenant); Set tenantSchemas = this.tenantConfigProvider.getAllTenantIds(); - // If we are in the last month of a quarter, generate the next quarter - if (isInLastMonthOfQuarter(currentQuarter, nextQuarter)) { + // If we are in the first month of a quarter, generate the next quarter + System.out.println("tenantSchemas = " + tenantSchemas); + if (isInFirstMonthOfQuarter(currentQuarter)) { for (String schema : tenantSchemas) { + // Set to the start month of the next Quarter + YearMonth nextQuarterYearMonth = currentYearMonth.plusMonths(3); logger.info("Generated quarters on last day of month for tenant {}", schema); - String label = createQuarterLabel(nextQuarterYearMonth, nextQuarter); + String label = createQuarterLabel(nextQuarterYearMonth, quarters.get(nextQuarterYearMonth.getMonthValue())); generateQuarter(nextQuarterYearMonth.atDay(1), label, schema); } } From 45205422256f94fcd328abf6257d248a9c28ef56 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Wed, 6 Aug 2025 08:00:52 +0200 Subject: [PATCH 02/13] fix: Calculation of the first month of the quarter should be dependent on the business year start #1556 --- .../business/QuarterBusinessService.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) 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 a186f51a9c..d8965e4c01 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 @@ -97,12 +97,10 @@ private void generateQuarter(LocalDate start, String label, String schema) { quarterPersistenceService.save(quarter); } - private boolean isInFirstMonthOfQuarter(int monthOfYear) { - // The first months of a quarter are 1, 4, 7, 10. - // In a modulo-3 calculation, these numbers all have a remainder of 1. - // (1 % 3 = 1, 4 % 3 = 1, 7 % 3 = 1, 10 % 3 = 1) - // All other months result in a remainder of 2 or 0. - return monthOfYear % 3 == 1; + private boolean isInFirstMonthOfQuarter(int currentQuarter, int endCurrentQuarter) { + // If we are still in the same quarter in 2 months, + // we are in the first month of the current quarter. + return Math.abs(endCurrentQuarter - currentQuarter) == 0; } public YearMonth getCurrentYearMonth() { @@ -123,21 +121,21 @@ Map generateQuarters() { @Scheduled(cron = "0 1 0 1 * ?") // Cron expression for 00:01:00 on the first day of every month public void scheduledGenerationQuarters() { Map quarters = generateQuarters(); - YearMonth currentYearMonth = getCurrentYearMonth().minusMonths(1); + YearMonth currentYearMonth = getCurrentYearMonth(); + YearMonth endCurrentQuarterYearMonth = currentYearMonth.plusMonths(2); int currentQuarter = quarters.get(currentYearMonth.getMonthValue()); + int nextQuarter = quarters.get(endCurrentQuarterYearMonth.getMonthValue()); String initialTenant = TenantContext.getCurrentTenant(); - System.out.println("initialTenant = " + initialTenant); Set tenantSchemas = this.tenantConfigProvider.getAllTenantIds(); // If we are in the first month of a quarter, generate the next quarter - System.out.println("tenantSchemas = " + tenantSchemas); - if (isInFirstMonthOfQuarter(currentQuarter)) { + if (isInFirstMonthOfQuarter(currentQuarter, nextQuarter)) { for (String schema : tenantSchemas) { // Set to the start month of the next Quarter - YearMonth nextQuarterYearMonth = currentYearMonth.plusMonths(3); - logger.info("Generated quarters on last day of month for tenant {}", schema); + YearMonth nextQuarterYearMonth = endCurrentQuarterYearMonth.plusMonths(1); + logger.info("Generated quarters on first day of month for tenant {}", schema); String label = createQuarterLabel(nextQuarterYearMonth, quarters.get(nextQuarterYearMonth.getMonthValue())); generateQuarter(nextQuarterYearMonth.atDay(1), label, schema); } From c33b670efa50ec07b75e65ae580f6a021db1c192 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Wed, 6 Aug 2025 08:10:52 +0200 Subject: [PATCH 03/13] style: run formatter --- .../ch/puzzle/okr/service/business/QuarterBusinessService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d8965e4c01..b7e97f5374 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 @@ -136,7 +136,8 @@ public void scheduledGenerationQuarters() { // Set to the start month of the next Quarter YearMonth nextQuarterYearMonth = endCurrentQuarterYearMonth.plusMonths(1); logger.info("Generated quarters on first day of month for tenant {}", schema); - String label = createQuarterLabel(nextQuarterYearMonth, quarters.get(nextQuarterYearMonth.getMonthValue())); + String label = createQuarterLabel(nextQuarterYearMonth, + quarters.get(nextQuarterYearMonth.getMonthValue())); generateQuarter(nextQuarterYearMonth.atDay(1), label, schema); } } From a644238411bfe8deab74e7f8af9e9195d6a8cb84 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Wed, 6 Aug 2025 08:15:10 +0200 Subject: [PATCH 04/13] test: Change test variables to match new logic --- .../business/QuarterBusinessServiceTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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..ceb2e4cc19 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 @@ -108,7 +108,7 @@ void shouldGetBacklogQuarter() { } @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 }) + @ValueSource(ints = { 2, 3, 5, 6, 8, 9, 11, 12 }) void shouldNotGenerateQuarterIfNotLastMonthOfQuarter(int month) { ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", 7); @@ -117,8 +117,8 @@ void shouldNotGenerateQuarterIfNotLastMonthOfQuarter(int month) { 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 }) + @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 shouldGenerateQuarterIfLastMonthOfQuarter(int month) { ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", 7); Mockito.doReturn(Set.of(TestHelper.SCHEMA_PITC)).when(tenantConfigProvider).getAllTenantIds(); @@ -130,12 +130,12 @@ void shouldGenerateQuarterIfLastMonthOfQuarter(int month) { 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 +147,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 From 5ffee847dd130604638c2bf3471f23ccd8e39707 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Thu, 7 Aug 2025 08:59:59 +0200 Subject: [PATCH 05/13] test: Change test variables to match new logic in persitence service --- .../persistence/QuarterPersistenceServiceIT.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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..b009a53cab 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,16 +138,20 @@ 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" }) + @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", "2,4,0", "3,4,0", "4,1,1", "5,1,0", "6,1,0", "7,2,1", "8,2,0", "9,2,0", "10,3,1", + "11,3,0", "12,3,0" }) void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfInvocations) { 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; + } Mockito.doReturn(YearMonth.of(nextYear, month)).when(quarterBusinessService).getCurrentYearMonth(); Mockito.doReturn(Set.of(TestHelper.SCHEMA_PITC)).when(tenantConfigProvider).getAllTenantIds(); From 26d6bd158c7ad7f87f1da88326963061086ef200 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 8 Aug 2025 13:37:08 +0200 Subject: [PATCH 06/13] refactor: Change the logic of the quarter generation of cron-job so it is more readable #1556 --- .../business/QuarterBusinessService.java | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) 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 b7e97f5374..3156edb139 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, String schema) { YearMonth yearMonth = YearMonth.from(start); Quarter quarter = Quarter.Builder @@ -95,16 +93,11 @@ private void generateQuarter(LocalDate start, String label, String schema) { .build(); validator.validateOnGeneration(quarter); quarterPersistenceService.save(quarter); - } - - private boolean isInFirstMonthOfQuarter(int currentQuarter, int endCurrentQuarter) { - // If we are still in the same quarter in 2 months, - // we are in the first month of the current quarter. - return Math.abs(endCurrentQuarter - currentQuarter) == 0; + return quarter; } public YearMonth getCurrentYearMonth() { - return YearMonth.now(); + return YearMonth.now().minusMonths(1); } Map generateQuarters() { @@ -118,30 +111,34 @@ Map generateQuarters() { return quarters; } - @Scheduled(cron = "0 1 0 1 * ?") // Cron expression for 00:01:00 on the first day of every month - public void scheduledGenerationQuarters() { - Map quarters = generateQuarters(); - YearMonth currentYearMonth = getCurrentYearMonth(); - YearMonth endCurrentQuarterYearMonth = currentYearMonth.plusMonths(2); + private Quarter getOrCreateCurrentQuarter(String schema) { + Quarter current = getCurrentQuarter(); + if (current == null) { + current = createQuarter(getCurrentYearMonth(), schema); + } + return current; + } - int currentQuarter = quarters.get(currentYearMonth.getMonthValue()); - int nextQuarter = quarters.get(endCurrentQuarterYearMonth.getMonthValue()); + private Quarter createQuarter(YearMonth creationDate, String schema) { + logger.info("Generated quarters on first day of month for tenant {}", schema); + String label = createQuarterLabel(creationDate, generateQuarters().get(creationDate.getMonthValue())); + return generateQuarter(creationDate.atDay(1), label, schema); + } + @Scheduled(cron = "0 1 0 1 * ?") // Runs at 00:01 on the 1st of each month + public void scheduledGenerationQuarters() { String initialTenant = TenantContext.getCurrentTenant(); - Set tenantSchemas = this.tenantConfigProvider.getAllTenantIds(); - // If we are in the first month of a quarter, generate the next quarter - if (isInFirstMonthOfQuarter(currentQuarter, nextQuarter)) { - for (String schema : tenantSchemas) { - // Set to the start month of the next Quarter - YearMonth nextQuarterYearMonth = endCurrentQuarterYearMonth.plusMonths(1); - logger.info("Generated quarters on first day of month for tenant {}", schema); - String label = createQuarterLabel(nextQuarterYearMonth, - quarters.get(nextQuarterYearMonth.getMonthValue())); - generateQuarter(nextQuarterYearMonth.atDay(1), label, schema); + for (String schema : tenantConfigProvider.getAllTenantIds()) { + TenantContext.setCurrentTenant(schema); + + Quarter currentQuarter = getOrCreateCurrentQuarter(schema); + if (getCurrentYearMonth().equals(YearMonth.from(currentQuarter.getStartDate()))) { + YearMonth nextQuarter = YearMonth.from(currentQuarter.getEndDate()).plusMonths(1); + createQuarter(nextQuarter, schema); } } TenantContext.setCurrentTenant(initialTenant); } -} +} \ No newline at end of file From dbf08827145521d4521c0a835e2a4d350370f7e9 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 8 Aug 2025 15:03:55 +0200 Subject: [PATCH 07/13] test: Add and update persistence and business service tests for Quarter Generation #1556 --- .../business/QuarterBusinessServiceTest.java | 28 ++++++++++-- .../QuarterPersistenceServiceIT.java | 43 ++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) 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 ceb2e4cc19..0349b81deb 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 @@ -12,6 +12,7 @@ import ch.puzzle.okr.service.validation.QuarterValidationService; import ch.puzzle.okr.test.TestHelper; import java.time.LocalDate; +import java.time.Year; import java.time.YearMonth; import java.util.*; import java.util.stream.Stream; @@ -107,24 +108,44 @@ 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}") + @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 even 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)); + 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 shouldGenerateQuarterIfLastMonthOfQuarter(int month) { + 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()); } @@ -207,7 +228,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 b009a53cab..862ebc54a7 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 @@ -139,7 +139,7 @@ void getModelNameShouldReturnQuarter() { } @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", "2,4,0", "3,4,0", "4,1,1", "5,1,0", "6,1,0", "7,2,1", "8,2,0", "9,2,0", "10,3,1", + @CsvSource(value = { "1,4,2", "2,4,0", "3,4,0", "4,1,2", "5,1,0", "6,1,0", "7,2,2", "8,2,0", "9,2,0", "10,3,2", "11,3,0", "12,3,0" }) void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfInvocations) { int startQuarter = 7; @@ -168,4 +168,45 @@ void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfI assertEquals(4 + amountOfInvocations, quarterBusinessService.getQuarters().size()); createdQuarters.forEach(quarter -> quarterPersistenceService.deleteById(quarter.getId())); } + + @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 shouldGenerateQuarterWithCronJob2(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; + 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(); + + quarterBusinessService.scheduledGenerationQuarters(); + + Mockito.verify(quarterPersistenceService, Mockito.times(amountOfInvocations)).save(ArgumentMatchers.any()); + + List createdQuarters = quarterPersistenceService + .findAll() + .stream() + .filter(quarter -> quarter.getLabel().equals(expectedLabel)) + .toList(); + assertEquals(amountOfInvocations, createdQuarters.size()); + assertEquals(4 + amountOfInvocations, quarterBusinessService.getQuarters().size()); + createdQuarters.forEach(quarter -> quarterPersistenceService.deleteById(quarter.getId())); + } } From 6ee79fa1ac7b7298b61f4b47aed48c5cb0eaa288 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 8 Aug 2025 15:04:47 +0200 Subject: [PATCH 08/13] style: Run formatter --- .../okr/service/business/QuarterBusinessServiceTest.java | 3 +-- .../service/persistence/QuarterPersistenceServiceIT.java | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) 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 0349b81deb..6fde9cc06c 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 @@ -12,7 +12,6 @@ import ch.puzzle.okr.service.validation.QuarterValidationService; import ch.puzzle.okr.test.TestHelper; import java.time.LocalDate; -import java.time.Year; import java.time.YearMonth; import java.util.*; import java.util.stream.Stream; @@ -136,7 +135,7 @@ void shouldGenerateQuarterIfFirstMonthOfQuarter(int month) { Mockito.when(quarterBusinessService.getCurrentYearMonth()).thenReturn(YearMonth.of(2030, month)); - LocalDate currentQuarterStart = LocalDate.of(2030,month,1); + LocalDate currentQuarterStart = LocalDate.of(2030, month, 1); LocalDate currentQuarterEnd = currentQuarterStart.plusMonths(3).minusDays(1); Quarter currentQuarter = new Quarter(); 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 862ebc54a7..d137028c8d 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 @@ -170,9 +170,10 @@ void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfI } @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 shouldGenerateQuarterWithCronJob2(int month, int quarterIndex, int amountOfInvocations, int currentQuarterStart) { + @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 shouldGenerateQuarterWithCronJob2(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(); @@ -184,7 +185,7 @@ void shouldGenerateQuarterWithCronJob2(int month, int quarterIndex, int amountOf expectedLabel = "GJ " + nextYearShort + "/" + (nextYearShort + 1) + "-Q" + quarterIndex; } - LocalDate start = LocalDate.of(nextYear,currentQuarterStart,1); + LocalDate start = LocalDate.of(nextYear, currentQuarterStart, 1); LocalDate end = start.plusMonths(3).minusDays(1); Quarter currentQuarter = new Quarter(); From 042010b5d77fdae767610bdeaccc77c733091b69 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 8 Aug 2025 15:45:51 +0200 Subject: [PATCH 09/13] test: Add tests for the double generation if the current quarter is missing #1556 --- .../business/QuarterBusinessServiceTest.java | 2 +- .../QuarterPersistenceServiceIT.java | 33 +------------------ 2 files changed, 2 insertions(+), 33 deletions(-) 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 6fde9cc06c..f27b6a2629 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 @@ -116,7 +116,7 @@ void shouldNotGenerateQuarterIfNotLastMonthOfQuarter(int month) { verify(quarterPersistenceService, never()).save(any()); } - @ParameterizedTest(name = "Should generate new quarter even if the current one does not exist on scheduledGenerationQuarters() when it is the first month of the quarter such as {0}") + @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); 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 d137028c8d..5fc89e6273 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,41 +138,10 @@ void getModelNameShouldReturnQuarter() { assertEquals(QUARTER, quarterPersistenceService.getModelName()); } - @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,2", "2,4,0", "3,4,0", "4,1,2", "5,1,0", "6,1,0", "7,2,2", "8,2,0", "9,2,0", "10,3,2", - "11,3,0", "12,3,0" }) - void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfInvocations) { - 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; - if (quarterIndex == 4) { - expectedLabel = "GJ " + (nextYearShort - 1) + "/" + nextYearShort + "-Q" + quarterIndex; - } else { - expectedLabel = "GJ " + nextYearShort + "/" + (nextYearShort + 1) + "-Q" + quarterIndex; - } - Mockito.doReturn(YearMonth.of(nextYear, month)).when(quarterBusinessService).getCurrentYearMonth(); - Mockito.doReturn(Set.of(TestHelper.SCHEMA_PITC)).when(tenantConfigProvider).getAllTenantIds(); - - quarterBusinessService.scheduledGenerationQuarters(); - - Mockito.verify(quarterPersistenceService, Mockito.times(amountOfInvocations)).save(ArgumentMatchers.any()); - - List createdQuarters = quarterPersistenceService - .findAll() - .stream() - .filter(quarter -> quarter.getLabel().equals(expectedLabel)) - .toList(); - assertEquals(amountOfInvocations, createdQuarters.size()); - assertEquals(4 + amountOfInvocations, quarterBusinessService.getQuarters().size()); - createdQuarters.forEach(quarter -> quarterPersistenceService.deleteById(quarter.getId())); - } - @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 shouldGenerateQuarterWithCronJob2(int month, int quarterIndex, int amountOfInvocations, + void shouldGenerateQuarterWithCronJob(int month, int quarterIndex, int amountOfInvocations, int currentQuarterStart) { int startQuarter = 7; ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", startQuarter); From f9daa1dddd440035a7263f88f5ccf3735c506d30 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 8 Aug 2025 15:46:27 +0200 Subject: [PATCH 10/13] style: Run formatter #1556 --- .../okr/service/persistence/QuarterPersistenceServiceIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5fc89e6273..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 @@ -142,7 +142,7 @@ void getModelNameShouldReturnQuarter() { @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 currentQuarterStart) { int startQuarter = 7; ReflectionTestUtils.setField(quarterBusinessService, "quarterStart", startQuarter); int nextYear = Year.now().atMonth(startQuarter).plusMonths(month + 12 - 1).getYear(); From 7447cc6d427a242a9f0ffb5392f35372143b8923 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 8 Aug 2025 17:22:18 +0200 Subject: [PATCH 11/13] refactor: Removed useless schema passing --- .../business/QuarterBusinessService.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) 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 3156edb139..b4f0d65810 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 @@ -82,7 +82,7 @@ private int getStartOfBusinessYear(YearMonth startOfQuarter, int quarter) { return startOfQuarter.minusMonths((quarter - 1) * 3L).getYear(); } - private Quarter generateQuarter(LocalDate start, String label, String schema) { + private Quarter generateQuarter(LocalDate start, String label) { YearMonth yearMonth = YearMonth.from(start); Quarter quarter = Quarter.Builder @@ -92,12 +92,11 @@ private Quarter generateQuarter(LocalDate start, String label, String schema) { .withEndDate(yearMonth.plusMonths(2).atEndOfMonth()) .build(); validator.validateOnGeneration(quarter); - quarterPersistenceService.save(quarter); - return quarter; + return quarterPersistenceService.save(quarter); } public YearMonth getCurrentYearMonth() { - return YearMonth.now().minusMonths(1); + return YearMonth.now(); } Map generateQuarters() { @@ -111,34 +110,37 @@ Map generateQuarters() { return quarters; } - private Quarter getOrCreateCurrentQuarter(String schema) { + private Quarter getOrCreateCurrentQuarter() { Quarter current = getCurrentQuarter(); if (current == null) { - current = createQuarter(getCurrentYearMonth(), schema); + current = createQuarter(getCurrentYearMonth()); } return current; } - private Quarter createQuarter(YearMonth creationDate, String schema) { - logger.info("Generated quarters on first day of month for tenant {}", schema); + private Quarter createQuarter(YearMonth creationDate) { String label = createQuarterLabel(creationDate, generateQuarters().get(creationDate.getMonthValue())); - return generateQuarter(creationDate.atDay(1), label, schema); + return generateQuarter(creationDate.atDay(1), label); } @Scheduled(cron = "0 1 0 1 * ?") // Runs at 00:01 on the 1st of each month public void scheduledGenerationQuarters() { + logger.warn("Start scheduling generation quarters"); String initialTenant = TenantContext.getCurrentTenant(); for (String schema : tenantConfigProvider.getAllTenantIds()) { + logger.warn("Start generating quarters on first day of month for tenant {}", schema); TenantContext.setCurrentTenant(schema); - Quarter currentQuarter = getOrCreateCurrentQuarter(schema); + Quarter currentQuarter = getOrCreateCurrentQuarter(); if (getCurrentYearMonth().equals(YearMonth.from(currentQuarter.getStartDate()))) { YearMonth nextQuarter = YearMonth.from(currentQuarter.getEndDate()).plusMonths(1); - createQuarter(nextQuarter, schema); + createQuarter(nextQuarter); } + logger.warn("Successfully generated quarters on first day of month for tenant {}", schema); } TenantContext.setCurrentTenant(initialTenant); + logger.warn("End scheduling generation quarters"); } } \ No newline at end of file From 753b577821b4a668be2d1bf23d75ba2b6311002e Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Mon, 11 Aug 2025 08:08:47 +0200 Subject: [PATCH 12/13] test: add mocks for save that now returns the generated quarter #1556 --- .../business/QuarterBusinessServiceTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 f27b6a2629..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 @@ -123,6 +123,15 @@ void shouldGenerateBothQuartersIfLastMonthOfQuarterAndCurrentQuarterDoesNotExist 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()); } @@ -181,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(); From 40c0d3f854d1f91a508361b3be6dea6f1b7a8cfb Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Fri, 15 Aug 2025 08:34:00 +0200 Subject: [PATCH 13/13] refactor: put long if condition into helper --- .../okr/service/business/QuarterBusinessService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 b4f0d65810..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 @@ -123,9 +123,14 @@ private Quarter createQuarter(YearMonth creationDate) { 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 scheduling generation quarters"); + logger.warn("Start scheduled generation of quarters"); String initialTenant = TenantContext.getCurrentTenant(); for (String schema : tenantConfigProvider.getAllTenantIds()) { @@ -133,7 +138,7 @@ public void scheduledGenerationQuarters() { TenantContext.setCurrentTenant(schema); Quarter currentQuarter = getOrCreateCurrentQuarter(); - if (getCurrentYearMonth().equals(YearMonth.from(currentQuarter.getStartDate()))) { + if (isFirstMonthOfQuarter(currentQuarter)) { YearMonth nextQuarter = YearMonth.from(currentQuarter.getEndDate()).plusMonths(1); createQuarter(nextQuarter); } @@ -141,6 +146,6 @@ public void scheduledGenerationQuarters() { } TenantContext.setCurrentTenant(initialTenant); - logger.warn("End scheduling generation quarters"); + logger.warn("End scheduled generation of quarters"); } } \ No newline at end of file