Remove disbursement details in undo loan disbursal action (#2609)

Co-authored-by: Jose Alberto Hernandez <alberto@MacBook-Pro.local>
This commit is contained in:
José Alberto Hernández
2022-09-27 13:15:59 -05:00
committed by GitHub
co-authored by Jose Alberto Hernandez
parent ee0184c0a5
commit d4517f4a4d
16 changed files with 307 additions and 69 deletions
@@ -564,11 +564,7 @@ public class TellerManagementReadPlatformServiceImpl implements TellerManagement
final LocalDate startDate = JdbcSupport.getLocalDate(rs, "start_date");
final LocalDate endDate = JdbcSupport.getLocalDate(rs, "end_date");
final Integer fullDayFromDB = rs.getInt("full_day");
Boolean fullDay = false;
if (fullDayFromDB == 1) {
fullDay = true;
}
final Boolean fullDay = rs.getBoolean("full_day");
final String startTime = rs.getString("start_time");
final String endTime = rs.getString("end_time");
@@ -25,6 +25,7 @@ public interface LoanApiConstants {
String disbursementDataParameterName = "disbursementData";
String disbursementDateParameterName = "expectedDisbursementDate";
String disbursementPrincipalParameterName = "principal";
String disbursementReversedParameterName = "reversed";
String disbursementNetDisbursalAmountParameterName = "netDisbursalAmount";
String updatedDisbursementDateParameterName = "updatedExpectedDisbursementDate";
String updatedDisbursementPrincipalParameterName = "updatedPrincipal";
@@ -959,7 +959,7 @@ final class LoansApiResourceSwagger {
public GetLoansLoanIdTimeline timeline;
public GetLoansLoanIdSummary summary;
public GetLoansLoanIdRepaymentSchedule repaymentSchedule;
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
@Schema(description = "Set of GetLoansLoanIdTransactions")
public Set<GetLoansLoanIdTransactions> transactions;
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
@@ -59,7 +59,6 @@ import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -143,7 +142,6 @@ import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDat
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
@Slf4j
@Entity
@Component
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
@@ -916,7 +914,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private Money getTotalAllTrancheDisbursementAmount() {
Money amount = Money.zero(getCurrency());
if (isMultiDisburmentLoan()) {
for (final LoanDisbursementDetails loanDisbursementDetail : this.disbursementDetails) {
for (final LoanDisbursementDetails loanDisbursementDetail : getDisbursementDetails()) {
amount = amount.plus(loanDisbursementDetail.principal());
}
}
@@ -1133,7 +1131,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
loanCharge.update(this);
if (this.loanProduct.isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) {
loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(this);
for (final LoanDisbursementDetails loanDisbursementDetails : this.disbursementDetails) {
for (final LoanDisbursementDetails loanDisbursementDetails : getDisbursementDetails()) {
if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().getId() == null) {
if (loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().equals(loanDisbursementDetails)) {
loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, loanDisbursementDetails);
@@ -1600,7 +1598,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
if (loanProduct.isDisallowExpectedDisbursements()) {
if (!disbursementDataArray.isEmpty()) {
if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details are not allowed";
throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
@@ -1609,11 +1607,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
}
if (disbursementDataArray.size() > loanProduct.maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage,
loanProduct.maxTrancheCount(), disbursementDetails.size());
if (disbursementDataArray.size() > loanProduct.maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage,
loanProduct.maxTrancheCount(), disbursementDetails.size());
}
}
} else {
this.disbursementDetails.clear();
@@ -1905,7 +1904,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (loanDisbursementDetail.actualDisbursementDate() == null) {
LocalDate actualDisbursementDate = null;
LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate,
principal, this.netDisbursalAmount);
principal, this.netDisbursalAmount, false);
disbursementDetails.updateLoan(this);
if (!loanDisbursementDetail.equals(disbursementDetails)) {
loanDisbursementDetail.copy(disbursementDetails);
@@ -1916,7 +1915,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} else {
LocalDate actualDisbursementDate = null;
LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate,
principal, this.netDisbursalAmount);
principal, this.netDisbursalAmount, false);
disbursementDetails.updateLoan(this);
this.disbursementDetails.add(disbursementDetails);
for (LoanTrancheCharge trancheCharge : trancheCharges) {
@@ -1959,7 +1958,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public LoanDisbursementDetails fetchLoanDisbursementsById(Long id) {
LoanDisbursementDetails loanDisbursementDetail = null;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (id.equals(disbursementDetail.getId())) {
loanDisbursementDetail = disbursementDetail;
break;
@@ -1970,7 +1969,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private List<Long> fetchDisbursementIds() {
List<Long> list = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetails : getDisbursementDetails()) {
list.add(disbursementDetails.getId());
}
return list;
@@ -2254,22 +2253,23 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
recalculateAllCharges();
if (loanProduct.isMultiDisburseLoan()) {
List<LoanDisbursementDetails> currentDisbursementDetails = getDisbursementDetails();
if (loanProduct.isDisallowExpectedDisbursements()) {
if (!disbursementDetails.isEmpty()) {
if (!currentDisbursementDetails.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details are not allowed";
throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
} else {
if (disbursementDetails.isEmpty()) {
if (currentDisbursementDetails.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
}
if (this.disbursementDetails.size() > loanProduct.maxTrancheCount()) {
if (currentDisbursementDetails.size() > loanProduct.maxTrancheCount()) {
final String errorMessage = "Number of tranche shouldn't be greter than " + loanProduct.maxTrancheCount();
throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage,
loanProduct.maxTrancheCount(), disbursementDetails.size());
loanProduct.maxTrancheCount(), currentDisbursementDetails.size());
}
}
this.approvedOnDate = approvedOn;
@@ -2463,7 +2463,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private List<LoanDisbursementDetails> getDisbursedLoanDisbursementDetails() {
List<LoanDisbursementDetails> ret = new ArrayList<>();
if (this.disbursementDetails != null && this.disbursementDetails.size() > 0) {
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (disbursementDetail.actualDisbursementDate() != null) {
ret.add(disbursementDetail);
}
@@ -2634,7 +2634,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private Collection<LoanDisbursementDetails> fetchUndisbursedDetail() {
Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>();
LocalDate date = null;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (disbursementDetail.actualDisbursementDate() == null) {
if (date == null || disbursementDetail.expectedDisbursementDate().compareTo(date) == 0 ? Boolean.TRUE : Boolean.FALSE) {
disbursementDetails.add(disbursementDetail);
@@ -2653,7 +2653,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
LoanDisbursementDetails details = null;
LocalDate date = this.actualDisbursementDate;
if (date != null) {
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (disbursementDetail.actualDisbursementDate() != null) {
if (disbursementDetail.actualDisbursementDate().isAfter(date)
|| disbursementDetail.actualDisbursementDate().compareTo(date) == 0 ? Boolean.TRUE : Boolean.FALSE) {
@@ -2668,7 +2668,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private boolean isDisbursementMissed() {
boolean isDisbursementMissed = false;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (disbursementDetail.actualDisbursementDate() == null
&& DateUtils.getBusinessLocalDate().isAfter(disbursementDetail.expectedDisbursementDateAsLocalDate())) {
isDisbursementMissed = true;
@@ -2680,7 +2680,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public BigDecimal getDisbursedAmount() {
BigDecimal principal = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (disbursementDetail.actualDisbursementDate() != null) {
principal = principal.add(disbursementDetail.principal());
}
@@ -2689,7 +2689,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
}
private void removeDisbursementDetail() {
Set<LoanDisbursementDetails> details = new HashSet<>(this.disbursementDetails);
Set<LoanDisbursementDetails> details = new HashSet<>(getDisbursementDetails());
for (LoanDisbursementDetails disbursementDetail : details) {
if (disbursementDetail.actualDisbursementDate() == null) {
this.disbursementDetails.remove(disbursementDetail);
@@ -2699,10 +2699,15 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private boolean isDisbursementAllowed() {
boolean isAllowed = false;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() == null) {
isAllowed = true;
break;
List<LoanDisbursementDetails> disbursementDetails = getDisbursementDetails();
if (disbursementDetails == null || disbursementDetails.size() == 0) {
isAllowed = true;
} else {
for (LoanDisbursementDetails disbursementDetail : disbursementDetails) {
if (disbursementDetail.actualDisbursementDate() == null) {
isAllowed = true;
break;
}
}
}
return isAllowed;
@@ -2710,7 +2715,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private boolean atleastOnceDisbursed() {
boolean isDisbursed = false;
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) {
if (disbursementDetail.actualDisbursementDate() != null) {
isDisbursed = true;
break;
@@ -2926,7 +2931,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
: Boolean.TRUE;
this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal);
if (this.loanProduct.isMultiDisburseLoan()) {
for (final LoanDisbursementDetails details : this.disbursementDetails) {
for (final LoanDisbursementDetails details : getDisbursementDetails()) {
details.updateActualDisbursementDate(null);
}
}
@@ -5094,10 +5099,20 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return list;
}
public List<LoanDisbursementDetails> getDisbursementDetails() {
public List<LoanDisbursementDetails> getAllDisbursementDetails() {
return this.disbursementDetails;
}
public List<LoanDisbursementDetails> getDisbursementDetails() {
List<LoanDisbursementDetails> currentDisbursementDetails = new ArrayList<LoanDisbursementDetails>();
for (LoanDisbursementDetails disbursementDetail : this.disbursementDetails) {
if (!disbursementDetail.isReversed()) {
currentDisbursementDetails.add(disbursementDetail);
}
}
return currentDisbursementDetails;
}
public ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final LoanDisbursementDetails disbursementDetails,
final JsonCommand command, final Map<String, Object> actualChanges, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final Locale locale = command.extractLocale();
@@ -5552,7 +5567,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
NthDayType nthDayType = null;
DayOfWeekType dayOfWeekType = null;
final List<DisbursementData> disbursementData = new ArrayList<>();
for (LoanDisbursementDetails disbursementDetails : this.disbursementDetails) {
for (LoanDisbursementDetails disbursementDetails : getDisbursementDetails()) {
disbursementData.add(disbursementDetails.toData());
}
@@ -6215,7 +6230,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
charge.resetToOriginal(loanCurrency());
}
}
for (final LoanDisbursementDetails details : this.disbursementDetails) {
for (final LoanDisbursementDetails details : getDisbursementDetails()) {
if (actualDisbursementDate.equals(details.actualDisbursementDate())) {
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(details.principal()));
details.updateActualDisbursementDate(null);
@@ -6278,7 +6293,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* get the next repayment LocalDate for rescheduling at the time of disbursement
*/
public LocalDate getNextPossibleRepaymentDateForRescheduling() {
List<LoanDisbursementDetails> loanDisbursementDetails = this.disbursementDetails;
List<LoanDisbursementDetails> loanDisbursementDetails = getDisbursementDetails();
LocalDate nextRepaymentDate = DateUtils.getBusinessLocalDate();
for (LoanDisbursementDetails loanDisbursementDetail : loanDisbursementDetails) {
if (loanDisbursementDetail.actualDisbursementDate() == null) {
@@ -49,16 +49,20 @@ public class LoanDisbursementDetails extends AbstractPersistableCustom {
@Column(name = "net_disbursal_amount", scale = 6, precision = 19)
private BigDecimal netDisbursalAmount;
@Column(name = "is_reversed", nullable = false)
private boolean reversed;
protected LoanDisbursementDetails() {
}
public LoanDisbursementDetails(final LocalDate expectedDisbursementDate, final LocalDate actualDisbursementDate,
final BigDecimal principal, final BigDecimal netDisbursalAmount) {
final BigDecimal principal, final BigDecimal netDisbursalAmount, final boolean reversed) {
this.expectedDisbursementDate = expectedDisbursementDate;
this.actualDisbursementDate = actualDisbursementDate;
this.principal = principal;
this.netDisbursalAmount = netDisbursalAmount;
this.reversed = reversed;
}
public void updateLoan(final Loan loan) {
@@ -88,6 +92,7 @@ public class LoanDisbursementDetails extends AbstractPersistableCustom {
this.principal = disbursementDetails.principal;
this.expectedDisbursementDate = disbursementDetails.expectedDisbursementDate;
this.actualDisbursementDate = disbursementDetails.actualDisbursementDate;
this.reversed = disbursementDetails.reversed;
}
public LocalDate expectedDisbursementDate() {
@@ -142,4 +147,11 @@ public class LoanDisbursementDetails extends AbstractPersistableCustom {
this.principal = principal;
}
public void reverse() {
this.reversed = true;
}
public boolean isReversed() {
return reversed;
}
}
@@ -0,0 +1,27 @@
/**
* 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.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanDisbursementDetailsRepository
extends JpaRepository<LoanDisbursementDetails, Long>, JpaSpecificationExecutor<LoanDisbursementDetails> {
}
@@ -1628,7 +1628,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
public Collection<DisbursementData> retrieveLoanDisbursementDetails(final Long loanId) {
final LoanDisbursementDetailMapper rm = new LoanDisbursementDetailMapper(sqlGenerator);
final String sql = "select " + rm.schema()
+ " where dd.loan_id=? group by dd.id, lc.amount_waived_derived order by dd.expected_disburse_date";
+ " where dd.loan_id=? and dd.is_reversed=false group by dd.id, lc.amount_waived_derived order by dd.expected_disburse_date";
return this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR
}
@@ -1960,7 +1960,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
sqlBuilder.append(" left join m_floating_rates bfr on bfr.is_base_lending_rate = true");
sqlBuilder.append(" left join m_floating_rates_periods bfrp on bfr.id = bfrp.floating_rates_id and bfrp.created_date >= ?");
sqlBuilder.append(" WHERE ml.loan_status_id = ? ");
sqlBuilder.append(" and ml.is_npa = false ");
sqlBuilder.append(" and ml.is_npa = false and dd.is_reversed = false ");
sqlBuilder.append(" and ((");
sqlBuilder.append("ml.interest_recalculation_enabled = true ");
sqlBuilder.append(" and (ml.interest_recalcualated_on is null or ml.interest_recalcualated_on <> ?)");
@@ -2008,7 +2008,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
sqlBuilder.append(" left join m_floating_rates bfr on bfr.is_base_lending_rate = true");
sqlBuilder.append(" left join m_floating_rates_periods bfrp on bfr.id = bfrp.floating_rates_id and bfrp.created_date >= ?");
sqlBuilder.append(" WHERE ml.loan_status_id = ? ");
sqlBuilder.append(" and ml.is_npa = false ");
sqlBuilder.append(" and ml.is_npa = false and dd.is_reversed = false ");
sqlBuilder.append(" and ((");
sqlBuilder.append("ml.interest_recalculation_enabled = true ");
sqlBuilder.append(" and (ml.interest_recalcualated_on is null or ml.interest_recalcualated_on <> ? )");
@@ -337,9 +337,13 @@ public class LoanUtilService {
netDisbursalAmount = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementNetDisbursalAmountParameterName)
.getAsBigDecimal();
}
disbursementDatas.add(
new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, principal, netDisbursalAmount));
boolean isReversed = false;
if (jsonObject.has(LoanApiConstants.disbursementReversedParameterName)) {
isReversed = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.disbursementReversedParameterName,
jsonObject);
}
disbursementDatas.add(new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, principal,
netDisbursalAmount, isReversed));
i++;
} while (i < disbursementDataArray.size());
}
@@ -164,6 +164,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetailsRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
@@ -269,7 +270,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanRepository loanRepository;
private final RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler;
private final PostDatedChecksRepository postDatedChecksRepository;
private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository;
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
@@ -325,9 +326,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// products
final LocalDate artificialExpectedDate = loan.getExpectedDisbursedOnLocalDate();
LoanDisbursementDetails disbursementDetail = new LoanDisbursementDetails(artificialExpectedDate, null,
loan.getDisbursedAmount(), null);
loan.getDisbursedAmount(), null, false);
disbursementDetail.updateLoan(loan);
loan.getDisbursementDetails().add(disbursementDetail);
loan.getAllDisbursementDetails().add(disbursementDetail);
}
}
loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED);
@@ -869,6 +870,19 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
// Remove All the Disbursement Details If the Loan Product is disabled and exists one
if (loan.loanProduct().isDisallowExpectedDisbursements()) {
if (!loan.getDisbursementDetails().isEmpty()) {
List<LoanDisbursementDetails> reversedDisbursementDetails = new ArrayList<LoanDisbursementDetails>();
for (LoanDisbursementDetails disbursementDetail : loan.getAllDisbursementDetails()) {
disbursementDetail.reverse();
reversedDisbursementDetails.add(disbursementDetail);
}
this.loanDisbursementDetailsRepository.saveAllAndFlush(reversedDisbursementDetails);
}
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan));
}
@@ -1237,6 +1237,8 @@ final class LoanProductsApiResourceSwagger {
@Schema(example = "50")
public Integer principalThresholdForLastInstalment;
public GetDelinquencyBucketsResponse delinquencyBucket;
@Schema(example = "true")
public Boolean disallowExpectedDisbursements;
}
@Schema(description = "PutLoanProductsProductIdRequest")
@@ -69,4 +69,5 @@
<include file="parts/0047_add_loan_delinquency_tags_business_step.xml" relativeToChangelogFile="true"/>
<include file="parts/0048_rework_loan_account_locks.xml" relativeToChangelogFile="true"/>
<include file="parts/0049_add_send_asynchronous_events_job.xml" relativeToChangelogFile="true"/>
<include file="parts/0050_add_reverse_flag_disbursement_details.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="1" author="fineract">
<addColumn tableName="m_loan_disbursement_detail">
<column name="is_reversed" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -25,6 +25,7 @@ import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.ArrayList;
import java.util.HashMap;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
@@ -75,7 +76,7 @@ public class ClientLoanNonTrancheMultipleDisbursementsIntegrationTest {
.withMultiDisburse() //
.withInterestCalculationPeriodTypeAsRepaymentPeriod(true) //
.withMaxTrancheCount("30") //
.withDisallowExpectectedDisbursements(true);
.withDisallowExpectedDisbursements(true);
if (isInterestRecalculationEnabled) {
final String interestRecalculationCompoundingMethod = LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE;
final String rescheduleStrategyMethod = LoanProductTestBuilder.RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS;
@@ -160,8 +161,12 @@ public class ClientLoanNonTrancheMultipleDisbursementsIntegrationTest {
LOG.info("-------------------------------DISBURSE non-tranch multi-disbursal loan ----------");
final String netDisbursedAmt = null;
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(submitDate, loanID, approved.toString(),
netDisbursedAmt);
loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithTransactionAmount(submitDate, loanID, approved.toString());
GetLoansLoanIdResponse getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID);
Assertions.assertNotNull(getLoansLoanIdResponse);
this.loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse);
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
@@ -35,9 +35,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
import org.apache.fineract.integrationtests.common.Utils;
@@ -271,8 +274,6 @@ public class LoanDisbursementDetailsIntegrationTest {
@Test
public void validateEqualInstallmentsForMultiTrancheLoan() {
List<HashMap> emptyTranches = new ArrayList<>();
final String operationDate = "01 January 2014";
final String principal = "1000";
final String disbursedPrincipal = "900";
@@ -284,7 +285,7 @@ public class LoanDisbursementDetailsIntegrationTest {
.withInterestTypeAsDecliningBalance().withMoratorium("", "").withInterestCalculationPeriodTypeAsRepaymentPeriod(true)
.withInterestTypeAsDecliningBalance() //
.withMultiDisburse() //
.withDisallowExpectectedDisbursements(true) //
.withDisallowExpectedDisbursements(true) //
.build(null);
log.info("Product {}", loanProductJSON);
final Integer loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
@@ -294,7 +295,7 @@ public class LoanDisbursementDetailsIntegrationTest {
log.info("-------------------LOAN CREATED WITH loanId----------------- {}", loanId);
this.loanTransactionHelper.approveLoanWithApproveAmount(operationDate, expectedDisbursementDate, principal, loanId, emptyTranches);
this.loanTransactionHelper.approveLoanWithApproveAmount(operationDate, expectedDisbursementDate, principal, loanId, null);
log.info("-------------------MULTI DISBURSAL LOAN APPROVED SUCCESSFULLY-------");
GetLoansLoanIdResponse getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
@@ -347,6 +348,101 @@ public class LoanDisbursementDetailsIntegrationTest {
this.editLoanDisbursementDetails();
}
@Test
public void allowModifyLoanApplicationAfterUndoDisbursalWithTranches() throws ParseException {
final String operationDate = this.approveDate;
List<HashMap> tranches = new ArrayList<>();
String principal = "1000";
final List<HashMap> collaterals = new ArrayList<>();
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, operationDate);
log.info("---------------------------------CLIENT CREATED WITH ID--------------------------------------------------- {}", clientId);
final Integer loanProductId = this.loanTransactionHelper
.getLoanProductId(new LoanProductTestBuilder().withInterestTypeAsDecliningBalance().withTranches(true)
.withDisallowExpectedDisbursements(true).withInterestCalculationPeriodTypeAsRepaymentPeriod(true).build(null));
log.info("----------------------------------LOAN PRODUCT CREATED WITH ID------------------------------------------- {}",
loanProductId);
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = this.loanTransactionHelper.getLoanProduct(loanProductId);
assertNotNull(getLoanProductsProductIdResponse);
log.info("Loan Product Id {} with DisallowExpectectedDisbursements in {}", loanProductId,
getLoanProductsProductIdResponse.getDisallowExpectedDisbursements());
assertEquals(true, getLoanProductsProductIdResponse.getDisallowExpectedDisbursements());
final Integer loanId = applyForLoanApplicationWithTranches(clientId, loanProductId, proposedAmount, tranches);
log.info("-----------------------------------LOAN CREATED WITH LOANID------------------------------------------------- {}", loanId);
loanTransactionHelper.approveLoanWithApproveAmount(operationDate, operationDate, approvalAmount, loanId, tranches);
GetLoansLoanIdResponse getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
log.info("-------------------MULTI DISBURSAL LOAN APPROVED SUCCESSFULLY-------");
assertEquals(0, getLoansLoanIdResponse.getDisbursementDetails().size(), "Disbursement details items");
loanTransactionHelper.disburseLoanWithTransactionAmount(operationDate, loanId, principal);
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
log.info("-------------------MULTI DISBURSAL LOAN DISBURSED SUCCESSFULLY-------");
assertEquals(1, getLoansLoanIdResponse.getDisbursementDetails().size(), "Disbursement details items");
PostLoansLoanIdResponse postLoansLoanIdResponse = this.loanTransactionHelper.applyLoanCommand(loanId, "undoDisbursal");
assertNotNull(postLoansLoanIdResponse);
log.info("-------------------UNDO DISBURSAL LOAN SUCCESSFULLY-------");
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
assertEquals(0, getLoansLoanIdResponse.getDisbursementDetails().size(), "Disbursement details items");
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
assertEquals(0, getLoansLoanIdResponse.getDisbursementDetails().size(), "Disbursement details items");
postLoansLoanIdResponse = this.loanTransactionHelper.applyLoanCommand(loanId, "undoApproval");
assertNotNull(postLoansLoanIdResponse);
log.info("-------------------UNDO APPROVAL LOAN SUCCESSFULLY-------");
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
principal = "10000";
final String loanApplicationJSON = buildLoanApplicationJSON(clientId, loanProductId, principal, tranches, operationDate,
collaterals);
log.info("Modify Loan Application: {}", loanApplicationJSON);
PutLoansLoanIdResponse putLoansLoanIdResponse = this.loanTransactionHelper.modifyLoanApplication(loanId, loanApplicationJSON);
assertNotNull(putLoansLoanIdResponse);
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {} and Principal {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size(),
getLoansLoanIdResponse.getPrincipal());
// ReDo the Approval and Disbursement
loanTransactionHelper.approveLoanWithApproveAmount(operationDate, operationDate, approvalAmount, loanId, null);
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
loanTransactionHelper.disburseLoanWithTransactionAmount(operationDate, loanId, principal);
getLoansLoanIdResponse = this.loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
assertNotNull(getLoansLoanIdResponse);
log.info("Loan Id {} with Status {} with Disbursement details {}", getLoansLoanIdResponse.getId(),
getLoansLoanIdResponse.getStatus().getCode(), getLoansLoanIdResponse.getDisbursementDetails().size());
log.info("-------------------MULTI DISBURSAL LOAN DISBURSED SUCCESSFULLY-------");
assertEquals(1, getLoansLoanIdResponse.getDisbursementDetails().size(), "Disbursement details items");
}
private void editLoanDisbursementDetails() throws ParseException {
this.editDateAndPrincipalOfExistingTranche();
this.addNewDisbursementDetails();
@@ -412,17 +508,10 @@ public class LoanDisbursementDetailsIntegrationTest {
return date;
}
private Integer applyForLoanApplicationWithTranches(final Integer clientId, final Integer loanProductId, String principal,
List<HashMap> tranches) {
log.info("----------------APPLYING FOR LOAN APPLICATION");
List<HashMap> collaterals = new ArrayList<>();
final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec);
Assertions.assertNotNull(collateralId);
final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec,
clientId.toString(), collateralId);
Assertions.assertNotNull(clientCollateralId);
addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1));
final String loanApplicationJSON = new LoanApplicationTestBuilder() //
private String buildLoanApplicationJSON(final Integer clientId, final Integer loanProductId, String principal, List<HashMap> tranches,
final String operationDate, List<HashMap> collaterals) {
return new LoanApplicationTestBuilder() //
.withPrincipal(principal) //
.withLoanTermFrequency("5") //
.withLoanTermFrequencyAsMonths() //
@@ -435,6 +524,20 @@ public class LoanDisbursementDetailsIntegrationTest {
.withInterestTypeAsDecliningBalance() //
.withSubmittedOnDate("01 March 2014") //
.withCollaterals(collaterals).build(clientId.toString(), loanProductId.toString(), null);
}
private Integer applyForLoanApplicationWithTranches(final Integer clientId, final Integer loanProductId, String principal,
List<HashMap> tranches) {
log.info("----------------APPLYING FOR LOAN APPLICATION");
List<HashMap> collaterals = new ArrayList<>();
final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec);
Assertions.assertNotNull(collateralId);
final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec,
clientId.toString(), collateralId);
Assertions.assertNotNull(clientCollateralId);
addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1));
final String loanApplicationJSON = buildLoanApplicationJSON(clientId, loanProductId, principal, tranches, "01 March 2014",
collaterals);
return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
}
@@ -394,7 +394,7 @@ public class LoanProductTestBuilder {
return this;
}
public LoanProductTestBuilder withDisallowExpectectedDisbursements(boolean disallowExpectectedDisbursements) {
public LoanProductTestBuilder withDisallowExpectedDisbursements(boolean disallowExpectectedDisbursements) {
this.disallowExpectedDisbursements = disallowExpectectedDisbursements;
if (this.disallowExpectedDisbursements) {
this.allowApprovedDisbursedAmountsOverApplied = true;
@@ -45,7 +45,9 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.client.models.PutLoansLoanIdResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.Utils;
@@ -136,6 +138,12 @@ public class LoanTransactionHelper {
"/fineract-provider/api/v1/loans/" + id + "?" + Utils.TENANT_IDENTIFIER, loanApplicationJSON, "loanId");
}
public PutLoansLoanIdResponse modifyLoanApplication(final Integer id, final String loanApplicationJSON) {
final String response = Utils.performServerPut(this.requestSpec, this.responseSpec,
"/fineract-provider/api/v1/loans/" + id + "?" + Utils.TENANT_IDENTIFIER, loanApplicationJSON, null);
return GSON.fromJson(response, PutLoansLoanIdResponse.class);
}
public ArrayList getLoanRepaymentSchedule(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer loanID) {
final String URL = "/fineract-provider/api/v1/loans/" + loanID + "?associations=repaymentSchedule&" + Utils.TENANT_IDENTIFIER;
@@ -319,6 +327,21 @@ public class LoanTransactionHelper {
getDisburseLoanAsJSON(date, null, netDisbursalAmount));
}
public PostLoansLoanIdResponse applyLoanCommand(final Integer loanId, final String command) {
String undoBodyJson = "{}";
String url = "";
if (command.equals(UNDO_APPROVAL_LOAN_COMMAND)) {
undoBodyJson = "{'note':'UNDO APPROVAL'}";
url = createLoanOperationURL(UNDO_APPROVAL_LOAN_COMMAND, loanId);
} else if (command.equals(UNDO_DISBURSE_LOAN_COMMAND)) {
undoBodyJson = "{'note' : 'UNDO DISBURSAL'}";
url = createLoanOperationURL(UNDO_DISBURSE_LOAN_COMMAND, loanId);
}
final String response = Utils.performServerPost(this.requestSpec, this.responseSpec, url, undoBodyJson, null);
log.info("Response {}", response);
return GSON.fromJson(response, PostLoansLoanIdResponse.class);
}
public HashMap undoDisbursal(final Integer loanID) {
final String undoDisburseJson = "{'note' : 'UNDO DISBURSAL'}";
log.info("IN DISBURSE LOAN");
@@ -807,6 +830,8 @@ public class LoanTransactionHelper {
}
private HashMap performLoanTransaction(final String postURLForLoanTransaction, final String jsonToBeSent) {
log.info("URL: {}", postURLForLoanTransaction);
log.info("Body: {}", jsonToBeSent);
final HashMap response = Utils.performServerPost(this.requestSpec, this.responseSpec, postURLForLoanTransaction, jsonToBeSent,
"changes");
return (HashMap) response.get("status");