FINERACT-1724: Add final accrual when loan is overpaid
This commit is contained in:
committed by
Arnold Gálovics
parent
0e3ad8f73b
commit
cefcb53424
+3
-4
@@ -3423,7 +3423,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
boolean isOverpaid = getTotalOverpaid() != null && getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
|
||||
if (isOverpaid) {
|
||||
// FIXME - kw - update account balance to negative amount.
|
||||
handleLoanOverpayment(loanLifecycleStateMachine);
|
||||
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else if (this.summary.isRepaidInFull(loanCurrency())) {
|
||||
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
|
||||
@@ -3536,10 +3536,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
|
||||
incomeDetailsMap.put(PENALTY, penalty);
|
||||
}
|
||||
|
||||
private void handleLoanOverpayment(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
|
||||
private void handleLoanOverpayment(LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
this.overpaidOnDate = transactionDate;
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, this);
|
||||
this.overpaidOnDate = DateUtils.getBusinessLocalDate();
|
||||
this.closedOnDate = null;
|
||||
this.actualMaturityDate = null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,5 +99,5 @@ public interface LoanAccountDomainService {
|
||||
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
|
||||
ExternalId externalId, PaymentDetail paymentDetail);
|
||||
|
||||
void applyIncomeAccrualTransaction(Loan loan);
|
||||
void applyFinalIncomeAccrualTransaction(Loan loan);
|
||||
}
|
||||
|
||||
+12
-2
@@ -877,7 +877,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyIncomeAccrualTransaction(Loan loan) {
|
||||
public void applyFinalIncomeAccrualTransaction(Loan loan) {
|
||||
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()
|
||||
// to avoid collision with processIncomeAccrualTransactionOnLoanClosure()
|
||||
&& !(loan.getLoanInterestRecalculationDetails() != null
|
||||
@@ -904,7 +904,9 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
if (total.isGreaterThanZero()) {
|
||||
ExternalId externalId = externalIdFactory.create();
|
||||
|
||||
LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), loan.getClosedOnDate(),
|
||||
LocalDate accrualTransactionDate = getFinalAccrualTransactionDate(loan);
|
||||
|
||||
LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), accrualTransactionDate,
|
||||
total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), externalId);
|
||||
|
||||
Set<LoanChargePaidBy> accrualCharges = accrualTransaction.getLoanChargesPaid();
|
||||
@@ -967,4 +969,12 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate getFinalAccrualTransactionDate(Loan loan) {
|
||||
return switch (loan.getStatus()) {
|
||||
case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
|
||||
case OVERPAID -> loan.getOverpaidOnDate();
|
||||
default -> throw new IllegalStateException("Unexpected value: " + loan.getStatus());
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -47,9 +47,9 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
|
||||
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
|
||||
final Loan loan = event.get();
|
||||
log.debug("Loan Status change for loan {}", loan.getId());
|
||||
if (loan.getStatus().isClosedObligationsMet()) {
|
||||
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
|
||||
log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId());
|
||||
loanAccountDomainService.applyIncomeAccrualTransaction(loan);
|
||||
loanAccountDomainService.applyFinalIncomeAccrualTransaction(loan);
|
||||
}
|
||||
if (loan.isOpen()) {
|
||||
loan.handleMaturityDateActivate();
|
||||
|
||||
+13
-14
@@ -1229,6 +1229,19 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
|
||||
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
|
||||
|
||||
if (loanTransaction.isReversed()) {
|
||||
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
|
||||
"Loan transaction:" + transactionId + " chargeback not allowed as loan transaction repayment is reversed",
|
||||
transactionId);
|
||||
}
|
||||
|
||||
if (!loanTransaction.isRepaymentLikeType()) {
|
||||
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
|
||||
"Loan transaction:" + transactionId + " chargeback not allowed as loan transaction is not repayment type, its type is "
|
||||
+ loanTransaction.getTypeOf().getCode(),
|
||||
transactionId);
|
||||
}
|
||||
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) {
|
||||
throw new PlatformServiceUnavailableException("error.msg.loan.transfer.transaction.update.not.allowed",
|
||||
@@ -1243,22 +1256,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
"Loan transaction:" + transactionId + " chargeback not allowed as loan product is interest recalculation enabled",
|
||||
transactionId);
|
||||
}
|
||||
|
||||
checkClientOrGroupActive(loan);
|
||||
|
||||
if (loanTransaction.isReversed()) {
|
||||
throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed",
|
||||
"Loan transaction:" + transactionId + " chargeback not allowed as loan transaction repayment is reversed",
|
||||
transactionId);
|
||||
}
|
||||
|
||||
if (!loanTransaction.isRepayment()) {
|
||||
throw new PlatformServiceUnavailableException(
|
||||
"error.msg.loan.chargeback.operation.not.allowed", "Loan transaction:" + transactionId
|
||||
+ " chargeback not allowed as loan transaction is not repayment, is " + loanTransaction.getTypeOf().getCode(),
|
||||
transactionId);
|
||||
}
|
||||
|
||||
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
|
||||
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
|
||||
|
||||
|
||||
+19
-11
@@ -610,17 +610,25 @@ public class DueDateRespectiveLoanRepaymentScheduleTest {
|
||||
assertEquals(100.0, response.getTotalOverpaid());
|
||||
assertTrue(response.getStatus().getOverpaid());
|
||||
|
||||
assertEquals(secondRepaymentId, response.getTransactions().get(2).getId().intValue());
|
||||
assertNull(response.getTransactions().get(2).getReversedOnDate());
|
||||
assertTrue(response.getTransactions().get(2).getTransactionRelations().isEmpty());
|
||||
assertTrue(response.getTransactions().get(2).getType().getRepayment());
|
||||
assertEquals(650.0, response.getTransactions().get(2).getAmount());
|
||||
assertEquals(550.0, response.getTransactions().get(2).getPrincipalPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(2).getPenaltyChargesPortion());
|
||||
assertEquals(100.0, response.getTransactions().get(2).getOverpaymentPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(2).getInterestPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(2).getFeeChargesPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(2).getOutstandingLoanBalance());
|
||||
int secondRepaymentIndex;
|
||||
// The repayment and accrual order is not consistent
|
||||
if (response.getTransactions().get(2).getType().getRepayment()) {
|
||||
secondRepaymentIndex = 2;
|
||||
} else {
|
||||
secondRepaymentIndex = 3;
|
||||
}
|
||||
|
||||
assertEquals(secondRepaymentId, response.getTransactions().get(secondRepaymentIndex).getId().intValue());
|
||||
assertNull(response.getTransactions().get(secondRepaymentIndex).getReversedOnDate());
|
||||
assertTrue(response.getTransactions().get(secondRepaymentIndex).getTransactionRelations().isEmpty());
|
||||
assertTrue(response.getTransactions().get(secondRepaymentIndex).getType().getRepayment());
|
||||
assertEquals(650.0, response.getTransactions().get(secondRepaymentIndex).getAmount());
|
||||
assertEquals(550.0, response.getTransactions().get(secondRepaymentIndex).getPrincipalPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(secondRepaymentIndex).getPenaltyChargesPortion());
|
||||
assertEquals(100.0, response.getTransactions().get(secondRepaymentIndex).getOverpaymentPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(secondRepaymentIndex).getInterestPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(secondRepaymentIndex).getFeeChargesPortion());
|
||||
assertEquals(0.0, response.getTransactions().get(secondRepaymentIndex).getOutstandingLoanBalance());
|
||||
|
||||
} finally {
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
|
||||
|
||||
+3
-10
@@ -30,8 +30,6 @@ import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
|
||||
@@ -57,12 +55,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
public class LoanAccountOverpaidDateStatusTest {
|
||||
|
||||
private ResponseSpecification responseSpec;
|
||||
private ResponseSpecification responseSpecErr400;
|
||||
private ResponseSpecification responseSpecErr503;
|
||||
private RequestSpecification requestSpec;
|
||||
private ClientHelper clientHelper;
|
||||
private LoanTransactionHelper loanTransactionHelper;
|
||||
private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter();
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
@@ -70,8 +65,6 @@ public class LoanAccountOverpaidDateStatusTest {
|
||||
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
this.responseSpecErr400 = new ResponseSpecBuilder().expectStatusCode(400).build();
|
||||
this.responseSpecErr503 = new ResponseSpecBuilder().expectStatusCode(503).build();
|
||||
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
|
||||
this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);
|
||||
}
|
||||
@@ -124,7 +117,7 @@ public class LoanAccountOverpaidDateStatusTest {
|
||||
GetLoansLoanIdResponse loanDetailsOverpaid = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetailsOverpaid.getStatus().getOverpaid());
|
||||
assertNotNull(loanDetailsOverpaid.getOverpaidOnDate());
|
||||
assertEquals(loanDetailsOverpaid.getOverpaidOnDate(), todaysDate);
|
||||
assertEquals(loanDetailsOverpaid.getOverpaidOnDate(), LocalDate.of(2022, 9, 9));
|
||||
|
||||
// reverse repayment to make loan not overpaid and overpaid date is reset
|
||||
loanTransactionHelper.reverseRepayment(loanId, repaymentTransaction_4.getResourceId().intValue(), "10 September 2022");
|
||||
@@ -141,7 +134,7 @@ public class LoanAccountOverpaidDateStatusTest {
|
||||
GetLoansLoanIdResponse loanDetailsOverpaid_1 = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetailsOverpaid_1.getStatus().getOverpaid());
|
||||
assertNotNull(loanDetailsOverpaid_1.getOverpaidOnDate());
|
||||
assertEquals(loanDetailsOverpaid_1.getOverpaidOnDate(), todaysDate);
|
||||
assertEquals(loanDetailsOverpaid_1.getOverpaidOnDate(), LocalDate.of(2022, 9, 11));
|
||||
|
||||
// Credit balance refund to reset overpaid status
|
||||
loanTransactionHelper.creditBalanceRefund("12 September 2022", Float.valueOf(100), null, loanId, "");
|
||||
@@ -164,7 +157,7 @@ public class LoanAccountOverpaidDateStatusTest {
|
||||
GetLoansLoanIdResponse loanDetailsOverpaid_3 = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||
assertTrue(loanDetailsOverpaid_3.getStatus().getOverpaid());
|
||||
assertNotNull(loanDetailsOverpaid_3.getOverpaidOnDate());
|
||||
assertEquals(loanDetailsOverpaid_3.getOverpaidOnDate(), todaysDate);
|
||||
assertEquals(loanDetailsOverpaid_3.getOverpaidOnDate(), LocalDate.of(2022, 9, 14));
|
||||
} finally {
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
|
||||
}
|
||||
|
||||
+78
@@ -427,6 +427,84 @@ public class LoanChargeSpecificDueDateTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApplyFeeAccrualWhenLoanOverpaid() {
|
||||
try {
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
|
||||
|
||||
final LocalDate todaysDate = Utils.getLocalDateOfTenant();
|
||||
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate);
|
||||
|
||||
// Client and Loan account creation
|
||||
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012");
|
||||
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithPeriodicAccrual(
|
||||
loanTransactionHelper, null);
|
||||
assertNotNull(getLoanProductsProductResponse);
|
||||
|
||||
LocalDate transactionDate = LocalDate.of(Utils.getLocalDateOfTenant().getYear(), 1, 1);
|
||||
String operationDate = Utils.dateFormatter.format(transactionDate);
|
||||
log.info("Disbursement date {}", transactionDate);
|
||||
|
||||
// Create Loan Account
|
||||
final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(),
|
||||
getLoanProductsProductResponse.getId().toString(), operationDate, "1", "0");
|
||||
|
||||
// Get loan details
|
||||
GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
||||
validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00"), true);
|
||||
|
||||
// Apply Loan Charge with specific due date
|
||||
String feeAmount = "10.00";
|
||||
String payloadJSON = ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, feeAmount, true);
|
||||
final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON);
|
||||
assertNotNull(postChargesResponse);
|
||||
final Long chargeId = postChargesResponse.getResourceId();
|
||||
assertNotNull(chargeId);
|
||||
|
||||
// First Loan Charge
|
||||
transactionDate = transactionDate.plusDays(1);
|
||||
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate);
|
||||
operationDate = Utils.dateFormatter.format(transactionDate);
|
||||
log.info("Operation date {}", transactionDate);
|
||||
payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(chargeId.toString(), operationDate, feeAmount);
|
||||
PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON,
|
||||
responseSpec);
|
||||
assertNotNull(postLoansLoanIdChargesResponse);
|
||||
final Long loanChargeId01 = postLoansLoanIdChargesResponse.getResourceId();
|
||||
assertNotNull(loanChargeId01);
|
||||
|
||||
Float amount = Float.valueOf("1020.00");
|
||||
transactionDate = transactionDate.plusDays(2);
|
||||
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate);
|
||||
operationDate = Utils.dateFormatter.format(transactionDate);
|
||||
log.info("Operation date {}", transactionDate);
|
||||
PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount,
|
||||
loanId);
|
||||
assertNotNull(loanIdTransactionsResponse);
|
||||
log.info("Loan Transaction Id: {} {}", loanId, loanIdTransactionsResponse.getResourceId());
|
||||
|
||||
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
||||
assertNotNull(getLoansLoanIdResponse);
|
||||
loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.overpaid");
|
||||
|
||||
final Long transactionId = loanTransactionHelper.evaluateLastLoanTransactionData(getLoansLoanIdResponse,
|
||||
"loanTransactionType.accrual", operationDate, Double.valueOf("10.00"));
|
||||
assertNotNull(transactionId);
|
||||
log.info("transactionId {}", transactionId);
|
||||
|
||||
final GetJournalEntriesTransactionIdResponse journalEntriesResponse = journalEntryHelper.getJournalEntries("L" + transactionId);
|
||||
assertNotNull(journalEntriesResponse);
|
||||
final List<JournalEntryTransactionItem> journalEntries = journalEntriesResponse.getPageItems();
|
||||
assertEquals(2, journalEntries.size());
|
||||
assertEquals(10, journalEntries.get(0).getAmount());
|
||||
assertEquals(10, journalEntries.get(1).getAmount());
|
||||
assertEquals(transactionDate, journalEntries.get(0).getTransactionDate());
|
||||
assertEquals(transactionDate, journalEntries.get(1).getTransactionDate());
|
||||
} finally {
|
||||
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper,
|
||||
final Integer delinquencyBucketId) {
|
||||
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId);
|
||||
|
||||
Reference in New Issue
Block a user