FINERACT-1971: Fix for infinite recursive loop in CalendarUtils when tenant and system TZ is different

This commit is contained in:
Arnold Galovics
2023-11-08 14:35:25 +01:00
committed by Arnold Gálovics
parent 9efe4f7ed5
commit f681ee9ca5
16 changed files with 593 additions and 25 deletions
@@ -61,7 +61,7 @@ public class ApplicationCurrency extends AbstractPersistableCustom {
currency.displaySymbol); currency.displaySymbol);
} }
private ApplicationCurrency(final String code, final String name, final int decimalPlaces, final Integer inMultiplesOf, public ApplicationCurrency(final String code, final String name, final int decimalPlaces, final Integer inMultiplesOf,
final String nameCode, final String displaySymbol) { final String nameCode, final String displaySymbol) {
this.code = code; this.code = code;
this.name = name; this.name = name;
@@ -38,11 +38,11 @@ public class MoneyHelper {
@Autowired @Autowired
private ConfigurationDomainService configurationDomainService; private ConfigurationDomainService configurationDomainService;
@PostConstruct
// This is a hack, but fixing this is not trivial, because some @Entity // This is a hack, but fixing this is not trivial, because some @Entity
// domain classes use this helper // domain classes use this helper
@PostConstruct
@SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD") @SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
public void someFunction() { public void initialize() {
staticConfigurationDomainService = configurationDomainService; staticConfigurationDomainService = configurationDomainService;
} }
@@ -25,11 +25,12 @@ import lombok.Getter;
@Getter @Getter
public enum RepaymentRescheduleType { public enum RepaymentRescheduleType {
INVALID(0, "RepaymentRescheduleType.invalid"), SAME_DAY(1, "RepaymentRescheduleType.same.day"), MOVE_TO_NEXT_WORKING_DAY(2, INVALID(0, "RepaymentRescheduleType.invalid"), //
"RepaymentRescheduleType.move.to.next.working.day"), MOVE_TO_NEXT_REPAYMENT_MEETING_DAY(3, SAME_DAY(1, "RepaymentRescheduleType.same.day"), //
"RepaymentRescheduleType.move.to.next.repayment.meeting.day"), MOVE_TO_PREVIOUS_WORKING_DAY(4, MOVE_TO_NEXT_WORKING_DAY(2, "RepaymentRescheduleType.move.to.next.working.day"), //
"RepaymentRescheduleType.move.to.previous.working.day"), MOVE_TO_NEXT_MEETING_DAY(5, MOVE_TO_NEXT_REPAYMENT_MEETING_DAY(3, "RepaymentRescheduleType.move.to.next.repayment.meeting.day"), //
"RepaymentRescheduleType.move.to.next.meeting.day"); MOVE_TO_PREVIOUS_WORKING_DAY(4, "RepaymentRescheduleType.move.to.previous.working.day"), //
MOVE_TO_NEXT_MEETING_DAY(5, "RepaymentRescheduleType.move.to.next.meeting.day"); //
private final Integer value; private final Integer value;
private final String code; private final String code;
@@ -51,7 +51,7 @@ public class WorkingDays extends AbstractPersistableCustom {
} }
protected WorkingDays(final String recurrence, final Integer repaymentReschedulingType, final Boolean extendTermForDailyRepayments, public WorkingDays(final String recurrence, final Integer repaymentReschedulingType, final Boolean extendTermForDailyRepayments,
final Boolean extendTermForRepaymentsOnHolidays) { final Boolean extendTermForRepaymentsOnHolidays) {
this.recurrence = recurrence; this.recurrence = recurrence;
this.repaymentReschedulingType = repaymentReschedulingType; this.repaymentReschedulingType = repaymentReschedulingType;
@@ -33,7 +33,7 @@ public final class WorkingDaysUtil {
public static LocalDate getOffSetDateIfNonWorkingDay(final LocalDate date, final LocalDate nextMeetingDate, public static LocalDate getOffSetDateIfNonWorkingDay(final LocalDate date, final LocalDate nextMeetingDate,
final WorkingDays workingDays) { final WorkingDays workingDays) {
// If date is not a non working day then return date. // If date is a working day then return date.
if (isWorkingDay(workingDays, date)) { if (isWorkingDay(workingDays, date)) {
return date; return date;
} }
@@ -32,6 +32,7 @@ import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.model.Date; import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList; import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.DateTime; import net.fortuna.ical4j.model.DateTime;
@@ -53,19 +54,18 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType;
import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType; import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType;
import org.apache.fineract.portfolio.common.domain.NthDayType; import org.apache.fineract.portfolio.common.domain.NthDayType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Slf4j
public final class CalendarUtils { public final class CalendarUtils {
public static final String FLOATING_TIMEZONE_PROPERTY_KEY = "net.fortuna.ical4j.timezone.date.floating";
private CalendarUtils() { private CalendarUtils() {
} }
private static final Logger LOG = LoggerFactory.getLogger(CalendarUtils.class);
static { static {
System.setProperty("net.fortuna.ical4j.timezone.date.floating", "true"); System.setProperty(FLOATING_TIMEZONE_PROPERTY_KEY, "true");
} }
public static LocalDateTime getNextRecurringDate(final String recurringRule, final LocalDateTime seedDate, public static LocalDateTime getNextRecurringDate(final String recurringRule, final LocalDateTime seedDate,
@@ -119,7 +119,7 @@ public final class CalendarUtils {
try { try {
formattedDate = new Date(seedDateStr, DateUtils.DEFAULT_DATETIME_FORMAT); formattedDate = new Date(seedDateStr, DateUtils.DEFAULT_DATETIME_FORMAT);
} catch (final ParseException e) { } catch (final ParseException e) {
LOG.error("Invalid date: {}", seedDateStr, e); log.error("Invalid date: {}", seedDateStr, e);
} }
return formattedDate; return formattedDate;
} }
@@ -170,15 +170,15 @@ public final class CalendarUtils {
numberOfDays); numberOfDays);
} }
private static Collection<LocalDate> convertToLocalDateList(final DateList dates, final LocalDate seedDate, static Collection<LocalDate> convertToLocalDateList(final DateList dates, final LocalDate seedDate,
final PeriodFrequencyType frequencyType, boolean isSkippMeetingOnFirstDay, final Integer numberOfDays) { final PeriodFrequencyType frequencyType, boolean isSkippMeetingOnFirstDay, final Integer numberOfDays) {
final Collection<LocalDate> recurringDates = new ArrayList<>(); final Collection<LocalDate> recurringDates = new ArrayList<>();
for (@SuppressWarnings("rawtypes") for (final Date date : dates) {
final Iterator iterator = dates.iterator(); iterator.hasNext();) { LocalDateTime dateTimeInProperTz = getLocalDateTimeFromICal4JDate(date);
final Date date = (Date) iterator.next(); ZoneId tenantZoneId = DateUtils.getDateTimeZoneOfTenant();
recurringDates.add((LocalDate) adjustDate(LocalDate.ofInstant(date.toInstant(), DateUtils.getDateTimeZoneOfTenant()), seedDate,
frequencyType)); recurringDates.add((LocalDate) adjustDate(dateTimeInProperTz.atZone(tenantZoneId).toLocalDate(), seedDate, frequencyType));
} }
if (isSkippMeetingOnFirstDay) { if (isSkippMeetingOnFirstDay) {
@@ -188,6 +188,12 @@ public final class CalendarUtils {
return recurringDates; return recurringDates;
} }
private static LocalDateTime getLocalDateTimeFromICal4JDate(Date date) {
java.util.Calendar cal = java.util.Calendar.getInstance();
cal.setTime(date);
return LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
}
private static Collection<LocalDate> skipMeetingOnFirstdayOfMonth(final Collection<LocalDate> recurringDates, private static Collection<LocalDate> skipMeetingOnFirstdayOfMonth(final Collection<LocalDate> recurringDates,
final Integer numberOfDays) { final Integer numberOfDays) {
final Collection<LocalDate> adjustedRecurringDates = new ArrayList<>(); final Collection<LocalDate> adjustedRecurringDates = new ArrayList<>();
@@ -220,10 +226,10 @@ public final class CalendarUtils {
return recur; return recur;
} catch (final ParseException e) { } catch (final ParseException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
LOG.error("Problem occurred in getICalRecur function", e); log.error("Problem occurred in getICalRecur function", e);
} catch (final ValidationException e) { } catch (final ValidationException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
LOG.error("Problem occurred in getICalRecur function", e); log.error("Problem occurred in getICalRecur function", e);
} }
return null; return null;
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.context;
import static org.apache.fineract.util.TimeZoneConstants.ASIA_KOLKATA_ID;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface WithTenantContext {
String tenantName() default "default";
String tenantTimeZoneId() default ASIA_KOLKATA_ID;
}
@@ -0,0 +1,59 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.context;
import java.lang.reflect.Method;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class WithTenantContextExtension implements BeforeEachCallback, AfterEachCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(WithTenantContextExtension.class);
private static final String ORIGINAL_TENANT_KEY = "originalTenant";
@Override
public void beforeEach(ExtensionContext context) throws Exception {
Method method = context.getTestMethod().orElse(null);
if (hasAnnotation(method)) {
context.getStore(NAMESPACE).put(ORIGINAL_TENANT_KEY, ThreadLocalContextUtil.getTenant());
WithTenantContext annotation = method.getAnnotation(WithTenantContext.class);
ThreadLocalContextUtil
.setTenant(new FineractPlatformTenant(1L, annotation.tenantName(), "Default", annotation.tenantTimeZoneId(), null));
}
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
Method method = context.getTestMethod().orElse(null);
if (hasAnnotation(method)) {
FineractPlatformTenant originalTenant = (FineractPlatformTenant) context.getStore(NAMESPACE).get(ORIGINAL_TENANT_KEY);
ThreadLocalContextUtil.setTenant(originalTenant);
}
}
private boolean hasAnnotation(Method method) {
return method != null && method.getAnnotation(WithTenantContext.class) != null;
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.system;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface WithSystemProperties {
WithSystemProperty[] value();
}
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.system;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@Repeatable(WithSystemProperties.class)
public @interface WithSystemProperty {
String key();
String value();
}
@@ -0,0 +1,82 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.system;
import java.lang.reflect.Method;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class WithSystemPropertyExtension implements BeforeEachCallback, AfterEachCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(WithSystemPropertyExtension.class);
private static final String ORIGINAL_SYSTEM_PROPERTY_KEY_PATTERN = "originalSystemProperty_%s";
@Override
public void beforeEach(ExtensionContext context) throws Exception {
Method method = context.getTestMethod().orElse(null);
if (hasAnnotation(method)) {
WithSystemProperty[] annotations = method.getAnnotationsByType(WithSystemProperty.class);
for (WithSystemProperty annotation : annotations) {
String propKey = annotation.key();
String propValue = annotation.value();
saveOriginalSystemPropertyValue(context, propKey);
overrideSystemPropertyValue(propKey, propValue);
}
}
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
Method method = context.getTestMethod().orElse(null);
if (hasAnnotation(method)) {
WithSystemProperty[] annotations = method.getAnnotationsByType(WithSystemProperty.class);
for (WithSystemProperty annotation : annotations) {
String propKey = annotation.key();
String propValue = loadOriginalSystemPropertyValue(context, propKey);
overrideSystemPropertyValue(propKey, propValue);
}
}
}
private void overrideSystemPropertyValue(String propKey, String propValue) {
if (propValue == null) {
System.clearProperty(propKey);
} else {
System.setProperty(propKey, propValue);
}
}
private void saveOriginalSystemPropertyValue(ExtensionContext context, String propKey) {
String originalKey = ORIGINAL_SYSTEM_PROPERTY_KEY_PATTERN.formatted(propKey);
context.getStore(NAMESPACE).put(originalKey, System.getProperty(propKey));
}
private String loadOriginalSystemPropertyValue(ExtensionContext context, String propKey) {
String originalKey = ORIGINAL_SYSTEM_PROPERTY_KEY_PATTERN.formatted(propKey);
return (String) context.getStore(NAMESPACE).get(originalKey);
}
private boolean hasAnnotation(Method method) {
return method != null && method.getAnnotation(WithSystemProperty.class) != null;
}
}
@@ -0,0 +1,31 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.timezone;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface WithSystemTimeZone {
String value();
}
@@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.junit.timezone;
import java.lang.reflect.Method;
import java.util.TimeZone;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class WithSystemTimeZoneExtension implements BeforeEachCallback, AfterEachCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(WithSystemTimeZoneExtension.class);
private static final String ORIGINAL_TIMEZONE_KEY = "originalTimeZone";
@Override
public void beforeEach(ExtensionContext context) throws Exception {
Method method = context.getTestMethod().orElse(null);
if (hasAnnotation(method)) {
context.getStore(NAMESPACE).put(ORIGINAL_TIMEZONE_KEY, TimeZone.getDefault());
String timeZoneId = getTimeZoneId(method);
TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId));
}
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
Method method = context.getTestMethod().orElse(null);
if (hasAnnotation(method)) {
TimeZone originalTimeZone = (TimeZone) context.getStore(NAMESPACE).get(ORIGINAL_TIMEZONE_KEY);
TimeZone.setDefault(originalTimeZone);
}
}
private String getTimeZoneId(Method method) {
return method.getAnnotation(WithSystemTimeZone.class).value();
}
private boolean hasAnnotation(Method method) {
return method != null && method.getAnnotation(WithSystemTimeZone.class) != null;
}
}
@@ -18,15 +18,36 @@
*/ */
package org.apache.fineract.portfolio.calendar.service; package org.apache.fineract.portfolio.calendar.service;
import static org.apache.fineract.portfolio.calendar.service.CalendarUtils.FLOATING_TIMEZONE_PROPERTY_KEY;
import static org.apache.fineract.portfolio.common.domain.PeriodFrequencyType.WEEKS;
import static org.apache.fineract.util.TimeZoneConstants.ASIA_MANILA_ID;
import static org.apache.fineract.util.TimeZoneConstants.EUROPE_BERLIN_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import java.text.ParseException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Month; import java.time.Month;
import java.time.temporal.ChronoField; import java.time.temporal.ChronoField;
import java.time.temporal.Temporal; import java.time.temporal.Temporal;
import java.util.Collection;
import java.util.List;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateList;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import net.fortuna.ical4j.model.parameter.Value;
import org.apache.fineract.junit.context.WithTenantContext;
import org.apache.fineract.junit.context.WithTenantContextExtension;
import org.apache.fineract.junit.system.WithSystemProperty;
import org.apache.fineract.junit.system.WithSystemPropertyExtension;
import org.apache.fineract.junit.timezone.WithSystemTimeZone;
import org.apache.fineract.junit.timezone.WithSystemTimeZoneExtension;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith({ WithSystemTimeZoneExtension.class, WithTenantContextExtension.class, WithSystemPropertyExtension.class })
public class CalendarUtilsTest { public class CalendarUtilsTest {
@Test @Test
@@ -120,7 +141,7 @@ public class CalendarUtilsTest {
Temporal seedDate = LocalDate.of(2023, Month.JANUARY, 27); Temporal seedDate = LocalDate.of(2023, Month.JANUARY, 27);
// when // when
Temporal adjustedDateWeekly = CalendarUtils.adjustDate(date, seedDate, PeriodFrequencyType.WEEKS); Temporal adjustedDateWeekly = CalendarUtils.adjustDate(date, seedDate, WEEKS);
// then // then
assertEquals(30, adjustedDateWeekly.get(ChronoField.DAY_OF_MONTH)); assertEquals(30, adjustedDateWeekly.get(ChronoField.DAY_OF_MONTH));
@@ -136,4 +157,46 @@ public class CalendarUtilsTest {
} }
@Test
@WithSystemTimeZone(ASIA_MANILA_ID)
@WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)
@WithSystemProperty(key = FLOATING_TIMEZONE_PROPERTY_KEY, value = "true")
public void testConvertToLocalDateListWorksForDifferentTimezones() throws ParseException {
// given
TimeZone timeZone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone("Asia/Manila");
DateList dates = new DateList(Value.DATE, timeZone);
dates.add(new Date("20231104", "yyyyMMdd"));
LocalDate seedDate = LocalDate.of(2023, 11, 4);
// when
Collection<LocalDate> result = CalendarUtils.convertToLocalDateList(dates, seedDate, WEEKS, false, 0);
// then
Collection<LocalDate> expected = List.of(LocalDate.of(2023, 11, 4));
assertThat(result).hasSize(1);
assertThat(result).isEqualTo(expected);
}
@Test
@WithSystemTimeZone(EUROPE_BERLIN_ID)
@WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)
@WithSystemProperty(key = FLOATING_TIMEZONE_PROPERTY_KEY, value = "true")
public void testConvertToLocalDateListWorksForSameTimezones() throws ParseException {
// given
TimeZone timeZone = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone("Europe/Berlin");
DateList dates = new DateList(Value.DATE, timeZone);
dates.add(new Date("20231104", "yyyyMMdd"));
LocalDate seedDate = LocalDate.of(2023, 11, 4);
// when
Collection<LocalDate> result = CalendarUtils.convertToLocalDateList(dates, seedDate, WEEKS, false, 0);
// then
Collection<LocalDate> expected = List.of(LocalDate.of(2023, 11, 4));
assertThat(result).hasSize(1);
assertThat(result).isEqualTo(expected);
}
} }
@@ -0,0 +1,136 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import static java.math.BigDecimal.ZERO;
import static java.util.Collections.EMPTY_LIST;
import static org.apache.fineract.organisation.monetary.domain.MonetaryCurrency.fromApplicationCurrency;
import static org.apache.fineract.organisation.workingdays.domain.RepaymentRescheduleType.MOVE_TO_NEXT_WORKING_DAY;
import static org.apache.fineract.portfolio.calendar.service.CalendarUtils.FLOATING_TIMEZONE_PROPERTY_KEY;
import static org.apache.fineract.portfolio.common.domain.DayOfWeekType.INVALID;
import static org.apache.fineract.portfolio.common.domain.PeriodFrequencyType.MONTHS;
import static org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.CUMULATIVE;
import static org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod.EQUAL_PRINCIPAL;
import static org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD;
import static org.apache.fineract.portfolio.loanproduct.domain.InterestMethod.FLAT;
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.NONE;
import static org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType.DISBURSEMENT_DATE;
import static org.apache.fineract.util.TimeZoneConstants.ASIA_MANILA_ID;
import static org.apache.fineract.util.TimeZoneConstants.EUROPE_BERLIN_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.junit.context.WithTenantContext;
import org.apache.fineract.junit.context.WithTenantContextExtension;
import org.apache.fineract.junit.system.WithSystemProperty;
import org.apache.fineract.junit.system.WithSystemPropertyExtension;
import org.apache.fineract.junit.timezone.WithSystemTimeZone;
import org.apache.fineract.junit.timezone.WithSystemTimeZoneExtension;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
@ExtendWith({ WithSystemTimeZoneExtension.class, WithTenantContextExtension.class, WithSystemPropertyExtension.class })
public class DefaultScheduledDateGeneratorTest {
private DefaultScheduledDateGenerator underTest = new DefaultScheduledDateGenerator();
@BeforeEach
public void setUp() {
ConfigurationDomainService cds = Mockito.mock(ConfigurationDomainService.class);
given(cds.getRoundingMode()).willReturn(6); // default
MoneyHelper moneyHelper = new MoneyHelper();
ReflectionTestUtils.setField(moneyHelper, "configurationDomainService", cds);
moneyHelper.initialize();
}
@Test
@WithSystemTimeZone(EUROPE_BERLIN_ID)
@WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)
@WithSystemProperty(key = FLOATING_TIMEZONE_PROPERTY_KEY, value = "true")
public void test_AdjustRepaymentDate_Works_WithSameTenant_And_SystemTimeZone() {
// given
HolidayDetailDTO holidayDetailDTO = createHolidayDTO();
LocalDate dueRepaymentPeriodDate = LocalDate.of(2023, 11, 26);
LoanApplicationTerms loanApplicationTerms = createLoanApplicationTerms(dueRepaymentPeriodDate, holidayDetailDTO);
// when
AdjustedDateDetailsDTO result = underTest.adjustRepaymentDate(dueRepaymentPeriodDate, loanApplicationTerms, holidayDetailDTO);
// then
assertThat(result.getChangedScheduleDate()).isEqualTo(LocalDate.of(2023, 11, 26));
assertThat(result.getChangedActualRepaymentDate()).isEqualTo(LocalDate.of(2023, 11, 26));
assertThat(result.getNextRepaymentPeriodDueDate()).isEqualTo(LocalDate.of(2023, 12, 26));
}
@Test
@WithSystemTimeZone(ASIA_MANILA_ID)
@WithTenantContext(tenantTimeZoneId = EUROPE_BERLIN_ID)
@WithSystemProperty(key = FLOATING_TIMEZONE_PROPERTY_KEY, value = "true")
public void test_AdjustRepaymentDate_Works_WithDifferentTenant_And_SystemTimeZone() {
// given
HolidayDetailDTO holidayDetailDTO = createHolidayDTO();
LocalDate dueRepaymentPeriodDate = LocalDate.of(2023, 11, 26);
LoanApplicationTerms loanApplicationTerms = createLoanApplicationTerms(dueRepaymentPeriodDate, holidayDetailDTO);
// when
AdjustedDateDetailsDTO result = underTest.adjustRepaymentDate(dueRepaymentPeriodDate, loanApplicationTerms, holidayDetailDTO);
// then
assertThat(result.getChangedScheduleDate()).isEqualTo(LocalDate.of(2023, 11, 26));
assertThat(result.getChangedActualRepaymentDate()).isEqualTo(LocalDate.of(2023, 11, 26));
assertThat(result.getNextRepaymentPeriodDueDate()).isEqualTo(LocalDate.of(2023, 12, 26));
}
private LoanApplicationTerms createLoanApplicationTerms(LocalDate dueRepaymentPeriodDate, HolidayDetailDTO holidayDetailDTO) {
ApplicationCurrency dollarCurrency = new ApplicationCurrency("USD", "US Dollar", 2, 0, "currency.USD", "$");
Money principalAmount = Money.of(fromApplicationCurrency(dollarCurrency), BigDecimal.valueOf(1000L));
LocalDate expectedDisbursementDate = LocalDate.of(2023, 10, 26);
LocalDate submittedOnDate = LocalDate.of(2023, 10, 24);
return LoanApplicationTerms.assembleFrom(dollarCurrency, 1, MONTHS, 1, 1, MONTHS, null, INVALID, EQUAL_PRINCIPAL, FLAT, ZERO,
MONTHS, ZERO, SAME_AS_REPAYMENT_PERIOD, false, principalAmount, expectedDisbursementDate, null, dueRepaymentPeriodDate,
null, null, null, null, null, Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST,
BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO,
null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false,
DISBURSEMENT_DATE, submittedOnDate, false, CUMULATIVE);
}
private HolidayDetailDTO createHolidayDTO() {
WorkingDays workingDays = new WorkingDays("FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU", MOVE_TO_NEXT_WORKING_DAY.getValue(),
false, false);
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(false, EMPTY_LIST, workingDays, false, false);
return holidayDetailDTO;
}
}
@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.util;
public final class TimeZoneConstants {
public static final String ASIA_KOLKATA_ID = "Asia/Kolkata";
public static final String ASIA_MANILA_ID = "Asia/Manila";
public static final String EUROPE_BERLIN_ID = "Europe/Berlin";
private TimeZoneConstants() {}
}