Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ private RescheduleLoansApiConstants() {

// approve action request parameters
public static final String approvedOnDateParam = "approvedOnDate";
public static final String waiveOverdueChargesParamName = "waiveOverdueCharges";

public static final Set<String> APPROVE_REQUEST_DATA_PARAMETERS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(localeParamName, dateFormatParamName, approvedOnDateParam)));
public static final Set<String> APPROVE_REQUEST_DATA_PARAMETERS = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(localeParamName, dateFormatParamName, approvedOnDateParam, waiveOverdueChargesParamName)));

public static final Set<String> commandParams = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(allCommandParamName, approveCommandParamName, pendingCommandParamName, rejectCommandParamName)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ public static final class PostUpdateRescheduleLoansRequest {
public String locale;
@Schema(example = "dd MMMM yyyy")
public String dateFormat;
@Schema(example = "true")
public Boolean waiveOverdueCharges;
}

@Schema(description = "PostCreateRescheduleLoansResponse ")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq
RescheduleLoansApiConstants.rejectedOnDateParam));
public static final Set<String> APPROVE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(RescheduleLoansApiConstants.localeParamName, RescheduleLoansApiConstants.dateFormatParamName,
RescheduleLoansApiConstants.approvedOnDateParam));
RescheduleLoansApiConstants.approvedOnDateParam, RescheduleLoansApiConstants.waiveOverdueChargesParamName));
private final FromJsonHelper fromJsonHelper;
@Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl")
private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate;
Expand Down Expand Up @@ -227,11 +227,21 @@ public void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBu

public static void validateForOverdueCharges(final DataValidatorBuilder dataValidatorBuilder, final Loan loan,
final LoanRepaymentScheduleInstallment installment) {
validateForOverdueCharges(dataValidatorBuilder, loan, installment, false);
}

public static void validateForOverdueCharges(final DataValidatorBuilder dataValidatorBuilder, final Loan loan,
final LoanRepaymentScheduleInstallment installment, final boolean waiveOverdueCharges) {
if (waiveOverdueCharges) {
return;
}
if (installment != null) {
LocalDate rescheduleFromDate = installment.getFromDate();
Collection<LoanCharge> charges = loan.getLoanCharges();
for (LoanCharge loanCharge : charges) {
if (loanCharge.isOverdueInstallmentCharge() && DateUtils.isAfter(loanCharge.getDueLocalDate(), rescheduleFromDate)) {
if (loanCharge.isActive() && loanCharge.isOverdueInstallmentCharge()
&& DateUtils.isAfter(loanCharge.getDueLocalDate(), rescheduleFromDate)
&& loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) {
dataValidatorBuilder.failWithCodeNoParameterAddedToErrorCode("not.allowed.due.to.overdue.charges");
break;
}
Expand Down Expand Up @@ -278,7 +288,7 @@ public void validateForCreateAction(final JsonCommand jsonCommand, final Loan lo

LoanRepaymentScheduleInstallment installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment, false);

if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
Expand Down Expand Up @@ -321,15 +331,18 @@ public void validateForApproveAction(final JsonCommand jsonCommand, LoanReschedu
validateApprovalDate(fromJsonHelper, loanRescheduleRequest, jsonElement, dataValidatorBuilder);
validateRescheduleRequestStatus(loanRescheduleRequest, dataValidatorBuilder);

final Boolean waiveOverdueCharges = fromJsonHelper.extractBooleanNamed(RescheduleLoansApiConstants.waiveOverdueChargesParamName,
jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.waiveOverdueChargesParamName).value(waiveOverdueCharges)
.ignoreIfNull().validateForBooleanValue();

LocalDate rescheduleFromDate = loanRescheduleRequest.getRescheduleFromDate();
final Loan loan = loanRescheduleRequest.getLoan();
LoanRepaymentScheduleInstallment installment;

validateLoanIsActive(loan, dataValidatorBuilder);
installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);
LoanRepaymentScheduleInstallment installment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(rescheduleFromDate);

validateReschedulingInstallment(dataValidatorBuilder, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment);
validateForOverdueCharges(dataValidatorBuilder, loan, installment, Boolean.TRUE.equals(waiveOverdueCharges));

if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
Expand Down Expand Up @@ -78,6 +79,7 @@
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
Expand Down Expand Up @@ -112,6 +114,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanChargeService loanChargeService;
private final LoanChargeWritePlatformService loanChargeWritePlatformService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanTermVariationsMapper loanTermVariationsMapper;
private final LoanScheduleComponent loanSchedule;
Expand Down Expand Up @@ -420,6 +423,19 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) {
final MathContext mathContext = MoneyHelper.getMathContext();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());

final Boolean waiveOverdueCharges = jsonCommand
.booleanObjectValueOfParameterNamed(RescheduleLoansApiConstants.waiveOverdueChargesParamName);

if (Boolean.TRUE.equals(waiveOverdueCharges)) {
for (final LoanCharge loanCharge : loan.getCharges()) {
if (loanCharge.isActive() && loanCharge.isOverdueInstallmentCharge()
&& loanCharge.getAmountOutstanding(loan.getCurrency()).isGreaterThanZero()) {
this.loanChargeWritePlatformService.waiveLoanCharge(loan.getId(), loanCharge.getId(), jsonCommand);
}
}
}

final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(),
loanApplicationTerms.getInterestMethod());
final LoanScheduleDTO loanScheduleDTO = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,4 +564,63 @@ private Long applyForLoanApplicationWithInterest(final Long clientId, final Long
Long loanId = loanTransactionHelper.applyLoan(loanRequest).getLoanId();
return loanId;
}

@Test
public void testApproveLoanRescheduleRequestWithWaiveOverdueCharges() {
approveLoanRescheduleRequestWithOverdueCharges(true);
}

@Test
public void testApproveLoanRescheduleRequestWithoutWaiveOverdueCharges() {
approveLoanRescheduleRequestWithOverdueCharges(false);
}

private void approveLoanRescheduleRequestWithOverdueCharges(final boolean waiveOverdueCharges) {
final AtomicReference<Long> loanIdRef = new AtomicReference<>();

runAt("01 January 2025", () -> {
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final Long productId = createLoanProductPeriodicWithInterest();
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), productId.intValue(),
BigDecimal.valueOf(1000.0), 4, 1, 4, BigDecimal.valueOf(2.0), "01 January 2025", "01 January 2025");
final Long loanId = loanResponse.getLoanId();
loanIdRef.set(loanId);

loanTransactionHelper.approveLoan("01 January 2025", loanId.intValue());
loanTransactionHelper.disburseLoan("01 January 2025", loanId.intValue(), "1000", null);
});

runAt("15 February 2025", () -> {
final Long loanId = loanIdRef.get();
final Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanOverdueFeeJSON());
loanTransactionHelper.addLoanCharge(loanId, new PostLoansLoanIdChargesRequest().chargeId(chargeId.longValue())
.dueDate("01 February 2025").amount(Double.valueOf("10.0")).dateFormat(DATETIME_PATTERN).locale("en"));

final PostCreateRescheduleLoansResponse rescheduleResponse = loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanId).rescheduleFromDate("01 March 2025")
.rescheduleReasonId(1L).submittedOnDate("15 February 2025").locale("en").dateFormat(DATETIME_PATTERN));
final Long rescheduleRequestId = rescheduleResponse.getResourceId();

final PostUpdateRescheduleLoansRequest approveRequest = new PostUpdateRescheduleLoansRequest()
.approvedOnDate("15 February 2025").locale("en").dateFormat(DATETIME_PATTERN).waiveOverdueCharges(waiveOverdueCharges);

if (waiveOverdueCharges) {
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleRequestId, approveRequest);

final GetLoanRescheduleRequestResponse getRescheduleResponse = loanRescheduleRequestHelper
.readLoanRescheduleRequest(rescheduleRequestId, null);
assertTrue(getRescheduleResponse.getStatus().getApproved());

final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
boolean chargeWaived = loanDetails.getCharges().stream()
.filter(c -> c.getIsPenalty() && c.getAmountOutstanding().compareTo(BigDecimal.ZERO) == 0 && c.getWaived())
.findAny().isPresent();
assertTrue(chargeWaived);
} else {
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
() -> loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleRequestId, approveRequest));
assertTrue(exception.getMessage().contains("not.allowed.due.to.overdue.charges"));
}
});
}
}
Loading