Skip to content

Commit 3198b1b

Browse files
committed
FINERACT-2208: Penalties are not recalculated after backdated transactions
1 parent 91f0662 commit 3198b1b

File tree

19 files changed

+1099
-234
lines changed

19 files changed

+1099
-234
lines changed

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,15 @@ public CommandWrapperBuilder adjustmentForLoanCharge(final Long loanId, final Lo
799799
return this;
800800
}
801801

802+
public CommandWrapperBuilder deactivateOverdueLoanCharges(final Long loanId, final Long loanChargeId) {
803+
this.actionName = "DEACTIVATEOVERDUE";
804+
this.entityName = "LOANCHARGE";
805+
this.entityId = loanChargeId;
806+
this.loanId = loanId;
807+
this.href = "/loans/" + loanId + "/charges/" + loanChargeId;
808+
return this;
809+
}
810+
802811
public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final Long loanChargeId) {
803812
this.actionName = "DELETE";
804813
this.entityName = "LOANCHARGE";

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ public static boolean isDateInTheFuture(final LocalDate localDate) {
302302
return isAfterBusinessDate(localDate);
303303
}
304304

305+
public static boolean isDateInThePast(final LocalDate localDate) {
306+
return isBeforeBusinessDate(localDate);
307+
}
308+
305309
public static int compare(LocalDate first, LocalDate second) {
306310
return compare(first, second, true);
307311
}
@@ -334,6 +338,10 @@ public static boolean isAfter(LocalDate first, LocalDate second) {
334338
return first != null && (second == null || first.isAfter(second));
335339
}
336340

341+
public static boolean isAfterInclusive(LocalDate first, LocalDate second) {
342+
return isAfter(first, second) || isEqual(first, second);
343+
}
344+
337345
public static long getDifference(LocalDate first, LocalDate second, @NotNull ChronoUnit unit) {
338346
if (first == null || second == null) {
339347
throw new IllegalArgumentException("Dates must not be null to get difference");

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.fineract.portfolio.loanaccount.domain;
2020

21+
import java.time.LocalDate;
22+
import java.util.List;
2123
import org.apache.fineract.infrastructure.core.domain.ExternalId;
2224
import org.springframework.data.jpa.repository.JpaRepository;
2325
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -30,4 +32,11 @@ public interface LoanChargeRepository extends JpaRepository<LoanCharge, Long>, J
3032

3133
@Query(FIND_ID_BY_EXTERNAL_ID)
3234
Long findIdByExternalId(@Param("externalId") ExternalId externalId);
35+
36+
@Query("""
37+
SELECT lc FROM LoanCharge lc
38+
WHERE lc.loan.id = :loanId
39+
AND lc.dueDate >= :fromDate
40+
""")
41+
List<LoanCharge> findByLoanIdAndFromDueDate(@Param("loanId") Long loanId, @Param("fromDate") LocalDate fromDate);
3342
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.exception;
20+
21+
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
22+
23+
public class LoanChargeDeactivationException extends AbstractPlatformDomainRuleException {
24+
25+
public LoanChargeDeactivationException(final String errorCode, final String defaultUserMessage,
26+
final Object... defaultUserMessageArgs) {
27+
super("error.msg." + errorCode, defaultUserMessage, defaultUserMessageArgs);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.handler;
20+
21+
import lombok.RequiredArgsConstructor;
22+
import org.apache.fineract.commands.annotation.CommandType;
23+
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
24+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
25+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
26+
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
27+
import org.springframework.stereotype.Service;
28+
import org.springframework.transaction.annotation.Transactional;
29+
30+
@Service
31+
@RequiredArgsConstructor
32+
@CommandType(entity = "LOANCHARGE", action = "DEACTIVATEOVERDUE")
33+
public class LoanChargeDeactivateOverdueCommandHandler implements NewCommandSourceHandler {
34+
35+
private final LoanChargeWritePlatformService writePlatformService;
36+
37+
@Transactional
38+
@Override
39+
public CommandProcessingResult processCommand(final JsonCommand command) {
40+
return writePlatformService.deactivateOverdueLoanCharge(command.getLoanId(), command);
41+
}
42+
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,7 @@ public interface LoanChargeWritePlatformService {
4141

4242
CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
4343

44+
CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, JsonCommand command);
45+
4446
void applyOverdueChargesForLoan(Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList);
4547
}

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanChargesApiResource.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class LoanChargesApiResource {
7979
public static final String COMMAND_PAY = "pay";
8080
public static final String COMMAND_WAIVE = "waive";
8181
public static final String COMMAND_ADJUSTMENT = "adjustment";
82+
public static final String COMMAND_DEACTIVATE_OVERDUE = "deactivateOverdue";
8283
private static final Set<String> RESPONSE_DATA_PARAMETERS = new HashSet<>(
8384
Arrays.asList("id", "chargeId", "name", "penalty", "chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage",
8485
"amountPercentageAppliedTo", "currency", "amountWaived", "amountWrittenOff", "amountOutstanding", "amountOrPercentage",
@@ -463,6 +464,10 @@ private String handleExecuteLoanCharge(final Long loanId, final String loanExter
463464
final CommandWrapper commandRequest = new CommandWrapperBuilder().payLoanCharge(resolvedLoanId, null)
464465
.withJson(apiRequestBodyAsJson).build();
465466
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
467+
} else if (CommandParameterUtil.is(commandParam, COMMAND_DEACTIVATE_OVERDUE)) {
468+
final CommandWrapper commandRequest = new CommandWrapperBuilder().deactivateOverdueLoanCharges(resolvedLoanId, null)
469+
.withJson(apiRequestBodyAsJson).build();
470+
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
466471
} else {
467472
final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanCharge(resolvedLoanId)
468473
.withJson(apiRequestBodyAsJson).build();

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ private void addAccruals(@NotNull Loan loan, @NotNull LocalDate tillDate, boolea
294294
|| !loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
295295
return;
296296
}
297+
297298
LoanInterestRecalculationDetails recalculationDetails = loan.getLoanInterestRecalculationDetails();
298299
if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) {
299300
return;
@@ -523,7 +524,9 @@ private void addChargeAccrual(@NotNull Loan loan, @NotNull LocalDate tillDate, b
523524
loanCharges = loan.getLoanCharges(lc -> !lc.isDueAtDisbursement() && (lc.isInstalmentFee() ? !DateUtils.isBefore(tillDate, dueDate)
524525
: isChargeDue(lc, tillDate, chargeOnDueDate, installment, period.isFirstPeriod())));
525526
for (LoanCharge loanCharge : loanCharges) {
526-
addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, installment, accrualPeriods);
527+
if (loanCharge.isActive()) {
528+
addChargeAccrual(loanCharge, tillDate, chargeOnDueDate, installment, accrualPeriods);
529+
}
527530
}
528531
}
529532

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
122122
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
123123
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeAdjustmentException;
124+
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeDeactivationException;
124125
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
125126
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
126127
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
@@ -130,6 +131,8 @@
130131
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
131132
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
132133
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
134+
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentParameter;
135+
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
133136
import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO;
134137
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
135138
import org.apache.fineract.portfolio.note.domain.Note;
@@ -174,6 +177,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
174177
private final LoanScheduleService loanScheduleService;
175178
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
176179
private final LoanAccountService loanAccountService;
180+
private final LoanAdjustmentService loanAdjustmentService;
177181

178182
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
179183
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
@@ -776,6 +780,49 @@ public CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanCha
776780
.build();
777781
}
778782

783+
@Transactional
784+
@Override
785+
public CommandProcessingResult deactivateOverdueLoanCharge(Long loanId, JsonCommand command) {
786+
LocalDate fromDueDate = command.dateValueOfParameterNamed("dueDate");
787+
788+
List<LoanCharge> loanCharges = loanChargeRepository.findByLoanIdAndFromDueDate(loanId, fromDueDate);
789+
loanCharges.forEach(this::inactivateOverdueLoanCharge);
790+
791+
Loan loan = loanAssembler.assembleFrom(loanId);
792+
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan
793+
.getRepaymentScheduleInstallments(si -> DateUtils.isDateInRangeInclusive(fromDueDate, si.getFromDate(), si.getDueDate())
794+
|| DateUtils.isAfter(si.getFromDate(), fromDueDate));
795+
repaymentScheduleInstallments.forEach(si -> si.setPenaltyAccrued(null));
796+
List<LoanTransaction> accrualsToReverse = loan.getLoanTransactions(
797+
tx -> tx.isNotReversed() && DateUtils.isAfterInclusive(tx.getTransactionDate(), fromDueDate) && tx.isAccrualRelated());
798+
accrualsToReverse.forEach(tx -> loanAdjustmentService.adjustLoanTransaction(loan, tx,
799+
LoanAdjustmentParameter.builder().transactionDate(tx.getTransactionDate()).build(), null, new HashMap<>()));
800+
801+
loanRepositoryWrapper.saveAndFlush(loan);
802+
803+
final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder();
804+
return commandProcessingResultBuilder.withLoanId(loanId) //
805+
.withEntityId(loanId) //
806+
.withEntityExternalId(loan.getExternalId()) //
807+
.build();
808+
}
809+
810+
private void inactivateOverdueLoanCharge(LoanCharge loanCharge) {
811+
if (!loanCharge.getChargeTimeType().isOverdueInstallment()) {
812+
throw new LoanChargeDeactivationException("loan.charge.deactivate.invalid.charge.type",
813+
"Loan charge is not an overdue installment charge");
814+
}
815+
816+
if (!loanCharge.isActive()) {
817+
throw new LoanChargeDeactivationException("loan.charge.deactivate.invalid.status", "Loan charge is not active");
818+
}
819+
820+
loanCharge.setActive(false);
821+
loanChargeRepository.saveAndFlush(loanCharge);
822+
823+
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge));
824+
}
825+
779826
@Transactional
780827
@Override
781828
public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList) {
@@ -802,8 +849,10 @@ public void applyOverdueChargesForLoan(final Long loanId, Collection<OverdueLoan
802849
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
803850
final JsonCommand command = JsonCommand.from(overdueInstallment.toString(), parsedCommand, this.fromApiJsonHelper, null, null,
804851
null, null, null, loanId, null, null, null, null, null, null, null, null);
852+
805853
LoanOverdueDTO overdueDTO = applyChargeToOverdueLoanInstallment(loan, overdueInstallment.getChargeId(),
806854
overdueInstallment.getPeriodNumber(), command);
855+
807856
loan = overdueDTO.getLoan();
808857
runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation();
809858
if (DateUtils.isAfter(recalculateFrom, overdueDTO.getRecalculateFrom())) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.service;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
import lombok.RequiredArgsConstructor;
25+
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
26+
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
27+
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
28+
import org.springframework.stereotype.Component;
29+
30+
@Component
31+
@RequiredArgsConstructor
32+
public class LoanJournalEntryPoster {
33+
34+
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
35+
36+
public void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
37+
final List<Long> existingReversedTransactionIds) {
38+
39+
final MonetaryCurrency currency = loan.getCurrency();
40+
boolean isAccountTransfer = false;
41+
List<Map<String, Object>> accountingBridgeData = new ArrayList<>();
42+
if (loan.isChargedOff()) {
43+
accountingBridgeData = loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds,
44+
existingReversedTransactionIds, isAccountTransfer);
45+
} else {
46+
accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
47+
existingReversedTransactionIds, isAccountTransfer));
48+
}
49+
for (Map<String, Object> accountingData : accountingBridgeData) {
50+
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData);
51+
}
52+
53+
}
54+
}

0 commit comments

Comments
 (0)