diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java index dadd2a81a..bd627cc92 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java @@ -45,7 +45,6 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.configuration.JobLocator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Lazy; import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean; @@ -71,10 +70,6 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi private static final HashMap SCHEDULERS = new HashMap<>(4); - @Autowired - @Lazy - private SchedulerStopListener schedulerStopListener; - @Autowired private FineractProperties fineractProperties; @@ -99,6 +94,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi final JobDetail jobDetail = createJobDetail(scheduledJobDetail); JobKey jobKey = jobDetail.getKey(); if (scheduler == null || !scheduler.checkExists(jobKey)) { + SchedulerStopListener schedulerStopListener = new SchedulerStopListener(this); final String tempSchedulerName = "temp" + scheduledJobDetail.getId(); final Scheduler tempScheduler = createScheduler(tempSchedulerName, 1, schedulerJobListener, schedulerStopListener); jobDataMap.put(SchedulerServiceConstants.SCHEDULER_NAME, tempSchedulerName); @@ -112,7 +108,7 @@ public class JobRegisterServiceImpl implements JobRegisterService, ApplicationLi } catch (final Exception e) { final String msg = "Job execution failed for job with id:" + scheduledJobDetail.getId(); log.error("{}", msg, e); - throw new PlatformInternalServerException("error.msg.sheduler.job.execution.failed", msg, scheduledJobDetail.getId(), e); + throw new PlatformInternalServerException("error.msg.scheduler.job.execution.failed", msg, scheduledJobDetail.getId(), e); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerStopListener.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerStopListener.java index e016393aa..c4488e995 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerStopListener.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerStopListener.java @@ -18,20 +18,16 @@ */ package org.apache.fineract.infrastructure.jobs.service; -import lombok.RequiredArgsConstructor; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobListener; -import org.springframework.stereotype.Component; /** * Global job Listener class to Stop the temporary scheduler once job execution completes */ -@Component -@RequiredArgsConstructor public class SchedulerStopListener implements JobListener { - private static final String name = "Singlr Trigger Global Listener"; + private static final String SINGLE_TRIGGER_GLOBAL_LISTENER = "Single Trigger Global Listener"; // MIFOSX-1184: This class cannot use constructor injection, because one of // its dependencies (SchedulerStopListener) has a circular dependency to @@ -47,7 +43,7 @@ public class SchedulerStopListener implements JobListener { @Override public String getName() { - return name; + return SINGLE_TRIGGER_GLOBAL_LISTENER; } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeNotFoundException.java index 2db934591..582c66d69 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeNotFoundException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeNotFoundException.java @@ -26,6 +26,10 @@ import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourc */ public class LoanChargeNotFoundException extends AbstractPlatformResourceNotFoundException { + public LoanChargeNotFoundException() { + super("error.msg.loanCharge.invalid", "Loan charge cannot be found"); + } + public LoanChargeNotFoundException(final Long id) { super("error.msg.loanCharge.id.invalid", "Loan charge with identifier " + id + " does not exist", id); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java index 1ed8e194e..32a9d557b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/exception/LoanChargeWaiveCannotBeReversedException.java @@ -29,38 +29,38 @@ public class LoanChargeWaiveCannotBeReversedException extends AbstractPlatformDo public String errorMessage() { - if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + if (name().equalsIgnoreCase("ALREADY_PAID")) { return "This loan charge has been completely paid"; - } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + } else if (name().equalsIgnoreCase("ALREADY_WAIVED")) { return "This loan charge has already been waived"; - } else if (name().toString().equalsIgnoreCase("LOAN_INACTIVE")) { + } else if (name().equalsIgnoreCase("LOAN_INACTIVE")) { return "This loan charge can be waived as the loan associated with it is currently inactive"; - } else if (name().toString().equalsIgnoreCase("WAIVE_NOT_ALLOWED_FOR_CHARGE")) { + } else if (name().equalsIgnoreCase("WAIVE_NOT_ALLOWED_FOR_CHARGE")) { return "This loan charge can be waived"; - } else if (name().toString().equalsIgnoreCase("NOT_WAIVED")) { + } else if (name().equalsIgnoreCase("NOT_WAIVED")) { return "This loan charge waive cannot be reversed as this charge is not waived"; - } else if (name().toString().equalsIgnoreCase("ALREADY_REVERSED")) { - return "This loan charge waive cannot be reversed as this transaction is not reversed"; + } else if (name().equalsIgnoreCase("ALREADY_REVERSED")) { + return "This loan charge waive cannot be reversed as this transaction is already reversed"; } - return name().toString(); + return name(); } public String errorCode() { - if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + if (name().equalsIgnoreCase("ALREADY_PAID")) { return "error.msg.loan.charge.already.paid"; - } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + } else if (name().equalsIgnoreCase("ALREADY_WAIVED")) { return "error.msg.loan.charge.already.waived"; - } else if (name().toString().equalsIgnoreCase("LOAN_INACTIVE")) { + } else if (name().equalsIgnoreCase("LOAN_INACTIVE")) { return "error.msg.loan.charge.associated.loan.inactive"; - } else if (name().toString().equalsIgnoreCase("WAIVE_NOT_ALLOWED_FOR_CHARGE")) { + } else if (name().equalsIgnoreCase("WAIVE_NOT_ALLOWED_FOR_CHARGE")) { return "error.msg.loan.charge.waive.not.allowed"; - } else if (name().toString().equalsIgnoreCase("NOT_WAIVED")) { + } else if (name().equalsIgnoreCase("NOT_WAIVED")) { return "error.msg.loan.charge.waive.cannot.undo"; - } else if (name().toString().equalsIgnoreCase("ALREADY_REVERSED")) { + } else if (name().equalsIgnoreCase("ALREADY_REVERSED")) { return "error.msg.transaction.cannot.reverse"; } - return name().toString(); + return name(); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index ca4fc1639..87be8ea40 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -35,6 +35,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -208,10 +209,10 @@ import org.springframework.util.CollectionUtils; @RequiredArgsConstructor public class LoansApiResource { - private final Set loanDataParameters = new HashSet<>(Arrays.asList("id", "accountNo", "status", "externalId", "clientId", - "group", "loanProductId", "loanProductName", "loanProductDescription", "isLoanProductLinkedToFloatingRate", "fundId", - "fundName", "loanPurposeId", "loanPurposeName", "loanOfficerId", "loanOfficerName", "currency", "principal", "totalOverpaid", - "inArrearsTolerance", "termFrequency", "termPeriodFrequencyType", "numberOfRepayments", "repaymentEvery", + private static final Set LOAN_DATA_PARAMETERS = new HashSet<>(Arrays.asList("id", "accountNo", "status", "externalId", + "clientId", "group", "loanProductId", "loanProductName", "loanProductDescription", "isLoanProductLinkedToFloatingRate", + "fundId", "fundName", "loanPurposeId", "loanPurposeName", "loanOfficerId", "loanOfficerName", "currency", "principal", + "totalOverpaid", "inArrearsTolerance", "termFrequency", "termPeriodFrequencyType", "numberOfRepayments", "repaymentEvery", "interestRatePerPeriod", "annualInterestRate", "repaymentFrequencyType", "transactionProcessingStrategyId", "transactionProcessingStrategyName", "interestRateFrequencyType", "amortizationType", "interestType", "interestCalculationPeriodType", LoanProductConstants.ALLOW_PARTIAL_PERIOD_INTEREST_CALCUALTION_PARAM_NAME, @@ -228,10 +229,10 @@ public class LoansApiResource { LoanApiConstants.datatables, LoanProductConstants.RATES_PARAM_NAME, LoanApiConstants.MULTIDISBURSE_DETAILS_PARAMNAME, LoanApiConstants.EMI_AMOUNT_VARIATIONS_PARAMNAME, LoanApiConstants.COLLECTION_PARAMNAME)); - private final Set loanApprovalDataParameters = new HashSet<>(Arrays.asList("approvalDate", "approvalAmount")); - final Set glimAccountsDataParameters = new HashSet<>(Arrays.asList("glimId", "groupId", "clientId", "parentLoanAccountNo", - "parentPrincipalAmount", "childLoanAccountNo", "childPrincipalAmount", "clientName")); - private final String resourceNameForPermissions = "LOAN"; + private static final Set LOAN_APPROVAL_DATA_PARAMETERS = new HashSet<>(Arrays.asList("approvalDate", "approvalAmount")); + private static final Set GLIM_ACCOUNTS_DATA_PARAMETERS = new HashSet<>(Arrays.asList("glimId", "groupId", "clientId", + "parentLoanAccountNo", "parentPrincipalAmount", "childLoanAccountNo", "childPrincipalAmount", "clientName")); + private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN"; private final PlatformSecurityContext context; private final LoanReadPlatformService loanReadPlatformService; @@ -281,7 +282,7 @@ public class LoansApiResource { @QueryParam("templateType") @Parameter(description = "templateType") final String templateType, @Context final UriInfo uriInfo) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); LoanApprovalData loanApprovalTemplate = null; @@ -293,7 +294,7 @@ public class LoansApiResource { } final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.loanApprovalDataToApiJsonSerializer.serialize(settings, loanApprovalTemplate, this.loanApprovalDataParameters); + return this.loanApprovalDataToApiJsonSerializer.serialize(settings, loanApprovalTemplate, LOAN_APPROVAL_DATA_PARAMETERS); } @@ -315,7 +316,7 @@ public class LoansApiResource { @DefaultValue("false") @QueryParam("activeOnly") @Parameter(description = "activeOnly") final boolean onlyActive, @Context final UriInfo uriInfo) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); // template final Collection productOptions = this.loanProductReadPlatformService.retrieveAllLoanProductsForLookup(onlyActive); @@ -412,7 +413,7 @@ public class LoansApiResource { newLoanAccount.setDatatables(datatableTemplates); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, newLoanAccount, this.loanDataParameters); + return this.toApiJsonSerializer.serialize(settings, newLoanAccount, LOAN_DATA_PARAMETERS); } private Collection getaccountLinkingOptions(final LoanAccountData newLoanAccount, final Long clientId, @@ -448,7 +449,7 @@ public class LoansApiResource { @QueryParam("exclude") @Parameter(in = ParameterIn.QUERY, name = "exclude", description = "Optional Loan object relation list to be filtered in the response", required = false, example = "guarantors,futureSchedule") final String exclude, @QueryParam("fields") @Parameter(in = ParameterIn.QUERY, name = "fields", description = "Optional Loan attribute list to be in the response", required = false, example = "id,principal,annualInterestRate") final String fields, @Context final UriInfo uriInfo) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); LoanAccountData loanBasicDetails = this.loanReadPlatformService.retrieveOne(loanId); if (loanBasicDetails.isInterestRecalculationEnabled()) { @@ -492,7 +493,7 @@ public class LoansApiResource { PortfolioAccountData linkedAccount = null; Collection disbursementData = null; Collection emiAmountVariations = null; - Collection loanCollateralManagements = null; + Collection loanCollateralManagements; Collection loanCollateralManagementData = new ArrayList<>(); CollectionData collectionData = CollectionData.template(); @@ -574,9 +575,6 @@ public class LoansApiResource { for (LoanCollateralResponseData loanCollateralManagement : loanCollateralManagements) { loanCollateralManagementData.add(loanCollateralManagement.toCommand()); } - if (CollectionUtils.isEmpty(loanCollateralManagements)) { - loanCollateralManagements = null; - } } if (associationParameters.contains(DataTableApiConstant.meetingAssociateParamName)) { @@ -606,7 +604,7 @@ public class LoansApiResource { } Collection productOptions = null; - LoanProductData product = null; + LoanProductData product; Collection loanTermFrequencyTypeOptions = null; Collection repaymentFrequencyTypeOptions = null; Collection repaymentFrequencyNthDayTypeOptions = null; @@ -624,7 +622,7 @@ public class LoansApiResource { Collection loanCollateralOptions = null; Collection calendarOptions = null; Collection accountLinkingOptions = null; - PaidInAdvanceData paidInAdvanceTemplate = null; + PaidInAdvanceData paidInAdvanceTemplate; Collection clientActiveLoanOptions = null; final boolean template = ApiParameterHelper.template(uriInfo.getQueryParameters()); @@ -640,7 +638,7 @@ public class LoansApiResource { amortizationTypeOptions = this.dropdownReadPlatformService.retrieveLoanAmortizationTypeOptions(); if (product.isLinkedToFloatingInterestRates()) { - interestTypeOptions = Arrays.asList(interestType(InterestMethod.DECLINING_BALANCE)); + interestTypeOptions = Collections.singletonList(interestType(InterestMethod.DECLINING_BALANCE)); } else { interestTypeOptions = this.dropdownReadPlatformService.retrieveLoanInterestTypeOptions(); } @@ -710,7 +708,7 @@ public class LoansApiResource { final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), mandatoryResponseParameters); - return this.toApiJsonSerializer.serialize(settings, loanAccount, this.loanDataParameters); + return this.toApiJsonSerializer.serialize(settings, loanAccount, LOAN_DATA_PARAMETERS); } @GET @@ -731,7 +729,7 @@ public class LoansApiResource { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder, @QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); final SearchParameters searchParameters = SearchParameters.forLoans(sqlSearch, externalId, offset, limit, orderBy, sortOrder, accountNo); @@ -739,7 +737,7 @@ public class LoansApiResource { final Page loanBasicDetails = this.loanReadPlatformService.retrieveAll(searchParameters); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, loanBasicDetails, this.loanDataParameters); + return this.toApiJsonSerializer.serialize(settings, loanBasicDetails, LOAN_DATA_PARAMETERS); } @POST @@ -767,7 +765,7 @@ public class LoansApiResource { final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, true); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule.toData(), new HashSet()); + return this.loanScheduleToApiJsonSerializer.serialize(settings, loanSchedule.toData(), new HashSet<>()); } final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanApplication().withJson(apiRequestBodyAsJson).build(); @@ -889,10 +887,10 @@ public class LoansApiResource { @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) public String getGlimRepaymentTemplate(@PathParam("glimId") final Long glimId, @Context final UriInfo uriInfo) { - this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); + this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS); Collection glimRepaymentTemplate = this.glimAccountInfoReadPlatformService.findglimRepaymentTemplate(glimId); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.glimTemplateToApiJsonSerializer.serialize(settings, glimRepaymentTemplate, this.glimAccountsDataParameters); + return this.glimTemplateToApiJsonSerializer.serialize(settings, glimRepaymentTemplate, GLIM_ACCOUNTS_DATA_PARAMETERS); } @POST diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 21728bfc7..f7dc17113 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -567,9 +567,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return this.summary; } - public void updateLoanSummaryForUndoWaiveCharge(final BigDecimal amountWaived) { - this.summary.updateFeeChargesWaived(this.summary.getTotalFeeChargesWaived().subtract(amountWaived)); - this.summary.updateFeeChargeOutstanding(this.summary.getTotalFeeChargesOutstanding().add(amountWaived)); + public void updateLoanSummaryForUndoWaiveCharge(final BigDecimal amountWaived, final boolean isPenalty) { + if (isPenalty) { + this.summary.updatePenaltyChargesWaived(this.summary.getTotalPenaltyChargesWaived().subtract(amountWaived)); + this.summary.updatePenaltyChargeOutstanding(this.summary.getTotalPenaltyChargesOutstanding().add(amountWaived)); + } else { + this.summary.updateFeeChargesWaived(this.summary.getTotalFeeChargesWaived().subtract(amountWaived)); + this.summary.updateFeeChargeOutstanding(this.summary.getTotalFeeChargesOutstanding().add(amountWaived)); + } + this.summary.updateTotalOutstanding(this.summary.getTotalOutstanding().add(amountWaived)); + this.summary.updateTotalWaived(this.summary.getTotalWaived().subtract(amountWaived)); } private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() { @@ -1377,7 +1384,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final Money principal = this.loanRepaymentScheduleDetail.getPrincipal(); this.summary.updateSummary(loanCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper, - isDisbursed(), this.charges); + this.charges); updateLoanOutstandingBalances(); } } @@ -2051,20 +2058,22 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (submittedOn.isAfter(DateUtils.getBusinessLocalDate())) { final String errorMessage = "The date on which a loan is submitted cannot be in the future."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn); + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn, + DateUtils.getBusinessLocalDate()); } if (this.client != null && this.client.isActivatedAfter(submittedOn)) { final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, - submittedOn); + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, submittedOn, + client.getActivationLocalDate()); } validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CREATED, submittedOn); if (this.group != null && this.group.isActivatedAfter(submittedOn)) { final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn); + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn, + group.getActivationLocalDate()); } if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 294966ce1..92f2b0d70 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -358,7 +358,7 @@ public class LoanCharge extends AbstractPersistableCustom { } } - public void resetOutstandingAmount(final BigDecimal amountOutstanding) { + public void setOutstandingAmount(final BigDecimal amountOutstanding) { this.amountOutstanding = amountOutstanding; } @@ -659,7 +659,7 @@ public class LoanCharge extends AbstractPersistableCustom { * amount is within min and max cap */ private BigDecimal minimumAndMaximumCap(final BigDecimal percentageOf) { - BigDecimal minMaxCap = BigDecimal.ZERO; + BigDecimal minMaxCap; if (this.minCap != null) { final int minimumCap = percentageOf.compareTo(this.minCap); if (minimumCap == -1) { @@ -789,7 +789,7 @@ public class LoanCharge extends AbstractPersistableCustom { * @return Actual amount paid on this charge */ public Money updatePaidAmountBy(final Money incrementBy, final Integer installmentNumber, final Money feeAmount) { - Money processAmount = Money.zero(incrementBy.getCurrency()); + Money processAmount; if (isInstalmentFee()) { if (installmentNumber == null) { processAmount = getUnpaidInstallmentLoanCharge().updatePaidAmountBy(incrementBy, feeAmount); @@ -802,7 +802,7 @@ public class LoanCharge extends AbstractPersistableCustom { Money amountPaidToDate = Money.of(processAmount.getCurrency(), this.amountPaid); final Money amountOutstanding = Money.of(processAmount.getCurrency(), this.amountOutstanding); - Money amountPaidOnThisCharge = Money.zero(processAmount.getCurrency()); + Money amountPaidOnThisCharge; if (processAmount.isGreaterThanOrEqualTo(amountOutstanding)) { amountPaidOnThisCharge = amountOutstanding; amountPaidToDate = amountPaidToDate.plus(amountOutstanding); @@ -890,7 +890,7 @@ public class LoanCharge extends AbstractPersistableCustom { public LoanInstallmentCharge getInstallmentLoanCharge(final Integer installmentNumber) { for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { - if (installmentNumber.equals(loanChargePerInstallment.getRepaymentInstallment().getInstallmentNumber().intValue())) { + if (installmentNumber.equals(loanChargePerInstallment.getRepaymentInstallment().getInstallmentNumber())) { return loanChargePerInstallment; } } @@ -900,7 +900,7 @@ public class LoanCharge extends AbstractPersistableCustom { public void setInstallmentLoanCharge(final LoanInstallmentCharge loanInstallmentCharge, final Integer installmentNumber) { LoanInstallmentCharge loanInstallmentChargeToBeRemoved = null; for (final LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { - if (installmentNumber.equals(loanChargePerInstallment.getRepaymentInstallment().getInstallmentNumber().intValue())) { + if (installmentNumber.equals(loanChargePerInstallment.getRepaymentInstallment().getInstallmentNumber())) { loanInstallmentChargeToBeRemoved = loanChargePerInstallment; break; } @@ -1017,7 +1017,7 @@ public class LoanCharge extends AbstractPersistableCustom { } public Money undoPaidOrPartiallyAmountBy(final Money incrementBy, final Integer installmentNumber, final Money feeAmount) { - Money processAmount = Money.zero(incrementBy.getCurrency()); + Money processAmount; if (isInstalmentFee()) { if (installmentNumber == null) { processAmount = getLastPaidOrPartiallyPaidInstallmentLoanCharge(incrementBy.getCurrency()).undoPaidAmountBy(incrementBy, @@ -1030,7 +1030,7 @@ public class LoanCharge extends AbstractPersistableCustom { } Money amountPaidToDate = Money.of(processAmount.getCurrency(), this.amountPaid); - Money amountDeductedOnThisCharge = Money.zero(processAmount.getCurrency()); + Money amountDeductedOnThisCharge; if (processAmount.isGreaterThanOrEqualTo(amountPaidToDate)) { amountDeductedOnThisCharge = amountPaidToDate; amountPaidToDate = Money.zero(processAmount.getCurrency()); @@ -1097,4 +1097,8 @@ public class LoanCharge extends AbstractPersistableCustom { this.externalId = externalId; } + public ChargeTimeType getChargeTimeType() { + return ChargeTimeType.fromInt(this.chargeTime); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java index b690795de..06ad91843 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -185,7 +185,7 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); final Money amountOutstanding = Money.of(incrementBy.getCurrency(), this.amountOutstanding); Money amountPaidPreviously = amountPaidToDate; - Money amountPaidOnThisCharge = Money.zero(incrementBy.getCurrency()); + Money amountPaidOnThisCharge; if (incrementBy.isGreaterThanOrEqualTo(amountOutstanding)) { amountPaidOnThisCharge = amountOutstanding; amountPaidToDate = amountPaidToDate.plus(amountOutstanding); @@ -226,7 +226,7 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements this.paid = false; } - public void resetAmountWaived(final BigDecimal amountWaived) { + public void setAmountWaived(final BigDecimal amountWaived) { this.amountWaived = amountWaived; } @@ -234,7 +234,7 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements this.waived = false; } - public void resetOutstandingAmount(final BigDecimal amountOutstanding) { + public void setOutstandingAmount(final BigDecimal amountOutstanding) { this.amountOutstanding = amountOutstanding; } @@ -293,7 +293,7 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); - Money amountToDeductOnThisCharge = Money.zero(incrementBy.getCurrency()); + Money amountToDeductOnThisCharge; if (incrementBy.isGreaterThanOrEqualTo(amountPaidToDate)) { amountToDeductOnThisCharge = amountPaidToDate; amountPaidToDate = Money.zero(incrementBy.getCurrency()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 7aea6ea41..071dcf377 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -20,7 +20,6 @@ package org.apache.fineract.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.time.LocalDate; -import java.util.Comparator; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; @@ -716,15 +715,6 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa } } - public static Comparator installmentNumberComparator = new Comparator() { - - @Override - public int compare(LoanRepaymentScheduleInstallment arg0, LoanRepaymentScheduleInstallment arg1) { - - return arg0.getInstallmentNumber().compareTo(arg1.getInstallmentNumber()); - } - }; - public BigDecimal getTotalPaidInAdvance() { return this.totalPaidInAdvance; } @@ -742,7 +732,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa public Money unpayPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); - Money penaltyPortionOfTransactionDeducted = Money.zero(currency); + Money penaltyPortionOfTransactionDeducted; final Money penaltyChargesCompleted = getPenaltyChargesPaid(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(penaltyChargesCompleted)) { @@ -761,7 +751,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa public Money unpayFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); - Money feePortionOfTransactionDeducted = Money.zero(currency); + Money feePortionOfTransactionDeducted; final Money feeChargesCompleted = getFeeChargesPaid(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(feeChargesCompleted)) { @@ -782,7 +772,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa public Money unpayInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); - Money interestPortionOfTransactionDeducted = Money.zero(currency); + Money interestPortionOfTransactionDeducted; final Money interestCompleted = getInterestPaid(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestCompleted)) { @@ -803,7 +793,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa public Money unpayPrincipalComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); - Money principalPortionOfTransactionDeducted = Money.zero(currency); + Money principalPortionOfTransactionDeducted; final Money principalCompleted = getPrincipalCompleted(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalCompleted)) { @@ -882,6 +872,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.feeChargesWaived = newFeeChargesCharged; } + public void setPenaltyChargesWaived(final BigDecimal newPenaltyChargesCharged) { + this.penaltyChargesWaived = newPenaltyChargesCharged; + } + public Set getInstallmentCharges() { return installmentCharges; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java index 902094070..9296dc06d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java @@ -35,7 +35,7 @@ import org.apache.fineract.organisation.monetary.domain.Money; */ @Embeddable @Getter -public final class LoanSummary { +public class LoanSummary { // derived totals fields @Column(name = "principal_disbursed_derived", scale = 6, precision = 19) @@ -126,7 +126,7 @@ public final class LoanSummary { return new LoanSummary(totalFeeChargesDueAtDisbursement); } - LoanSummary() { + protected LoanSummary() { // } @@ -150,10 +150,18 @@ public final class LoanSummary { this.totalFeeChargesOutstanding = totalFeeChargesOutstanding; } + public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) { + this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding; + } + public void updateFeeChargesWaived(final BigDecimal totalFeeChargesWaived) { this.totalFeeChargesWaived = totalFeeChargesWaived; } + public void updatePenaltyChargesWaived(final BigDecimal totalPenaltyChargesWaived) { + this.totalPenaltyChargesWaived = totalPenaltyChargesWaived; + } + public boolean isRepaidInFull(final MonetaryCurrency currency) { return getTotalOutstanding(currency).isZero(); } @@ -182,6 +190,14 @@ public final class LoanSummary { return this.totalOutstanding; } + public void updateTotalOutstanding(final BigDecimal newTotalOutstanding) { + this.totalOutstanding = newTotalOutstanding; + } + + public void updateTotalWaived(final BigDecimal totalWaived) { + this.totalWaived = totalWaived; + } + /** * All fields but totalFeeChargesDueAtDisbursement should be reset. */ @@ -217,7 +233,7 @@ public final class LoanSummary { public void updateSummary(final MonetaryCurrency currency, final Money principal, final List repaymentScheduleInstallments, final LoanSummaryWrapper summaryWrapper, - final Boolean disbursed, Set charges) { + Set charges) { this.totalPrincipalDisbursed = principal.getAmount(); this.totalPrincipalAdjustments = summaryWrapper.calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java index bdd343013..16f6268cf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java @@ -18,9 +18,11 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface LoanTransactionRepository extends JpaRepository, JpaSpecificationExecutor { - // no added behaviour + + Optional findByIdAndLoanId(Long transactionId, Long loanId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index b8ebd1293..349546f1c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -234,6 +234,7 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { + public static final String AMOUNT = "amount"; private final PlatformSecurityContext context; private final LoanEventApiJsonValidator loanEventApiJsonValidator; private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer; @@ -278,6 +279,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository; private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository; + private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { + return e.getFromDate().isBefore(loanCharge.getDueDate()) && !loanCharge.getDueDate().isAfter(e.getDueDate()); + } + private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService); } @@ -343,7 +348,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Set loanCollateralManagements = loan.getLoanCollateralManagements(); // Get relevant loan collateral modules - if ((loanCollateralManagements != null && loanCollateralManagements.size() != 0) + if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) && AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { BigDecimal totalCollateral = BigDecimal.valueOf(0); @@ -463,7 +468,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.addLoanTransaction(disbursementTransaction); } - if (loan.getRepaymentScheduleInstallments().size() == 0) { + if (loan.getRepaymentScheduleInstallments().isEmpty()) { /* * If no schedule, generate one (applicable to non-tranche multi-disbursal loans) */ @@ -578,7 +583,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf * * @param loan * the disbursed loan - * **/ private void createStandingInstruction(Loan loan) { @@ -877,15 +881,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf 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 reversedDisbursementDetails = new ArrayList(); - for (LoanDisbursementDetails disbursementDetail : loan.getAllDisbursementDetails()) { - disbursementDetail.reverse(); - reversedDisbursementDetails.add(disbursementDetail); - } - this.loanDisbursementDetailsRepository.saveAllAndFlush(reversedDisbursementDetails); + if (loan.loanProduct().isDisallowExpectedDisbursements() && !loan.getDisbursementDetails().isEmpty()) { + List reversedDisbursementDetails = new ArrayList<>(); + for (LoanDisbursementDetails disbursementDetail : loan.getAllDisbursementDetails()) { + disbursementDetail.reverse(); + reversedDisbursementDetails.add(disbursementDetail); } + this.loanDisbursementDetailsRepository.saveAllAndFlush(reversedDisbursementDetails); } businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan)); @@ -1924,10 +1926,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Integer installmentNumber = installmentChargeEntry.getRepaymentInstallment().getInstallmentNumber(); chargeAmountPaid = installmentChargeEntry.getAmountPaid(loanCurrency).getAmount(); for (LoanChargePaidBy loanChargePaidBy : loanCharge.getLoanChargePaidBySet()) { - if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber())) { - if (isRefundElementOfChargeRefund(loanChargePaidBy)) { - chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); - } + if (installmentNumber.equals(loanChargePaidBy.getInstallmentNumber()) && isRefundElementOfChargeRefund(loanChargePaidBy)) { + chargeAmountRefunded = chargeAmountRefunded.add(loanChargePaidBy.getAmount()); } } } else { @@ -1947,12 +1947,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } BigDecimal refundableAmount = chargeAmountPaid.subtract(chargeAmountRefunded); - if (transactionAmount != null) { // refund amount was provided. - if (transactionAmount.compareTo(refundableAmount) > 0) { - final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable"; - final String details = "transactionAmount: " + transactionAmount + " Refundable: " + refundableAmount; - throw new LoanChargeRefundException(errorMessage, details); - } + // refund amount was provided. + if (transactionAmount != null && transactionAmount.compareTo(refundableAmount) > 0) { + final String errorMessage = "loan.charge.transaction.amount.is.more.than.is.refundable"; + final String details = "transactionAmount: " + transactionAmount + " Refundable: " + refundableAmount; + throw new LoanChargeRefundException(errorMessage, details); } return refundableAmount; @@ -2022,145 +2021,155 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf @Override public CommandProcessingResult undoWaiveLoanCharge(final JsonCommand command) { - LoanTransaction loanTransaction = this.loanTransactionRepository.findById(command.entityId()) - .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId())); - + LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) + .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); if (!loanTransaction.getTypeOf().getCode().equals(LoanTransactionType.WAIVE_CHARGES.getCode())) { - throw new InvalidLoanTransactionTypeException("Undo Waive Charge", "Waive an Installment Charge First", - "Transaction is not a waive charge type."); + throw new InvalidLoanTransactionTypeException("transaction", "undo.waive.charge", "Transaction is not a waive charge type."); + } + if (!loanTransaction.isNotReversed()) { + throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED, loanTransaction.getId()); } Set loanChargePaidBySet = loanTransaction.getLoanChargesPaid(); - Integer installmentNumber = null; - Long loanChargeId = null; - final Long loanId = loanTransaction.getLoan().getId(); - - for (LoanChargePaidBy loanChargePaidBy : loanChargePaidBySet) { - installmentNumber = loanChargePaidBy.getInstallmentNumber(); - loanChargeId = loanChargePaidBy.getLoanCharge().getId(); - break; + LoanChargePaidBy loanChargePaidBy = loanChargePaidBySet.stream().findFirst().orElseThrow(LoanChargeNotFoundException::new); + final LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); + // Validate loan charge is not already paid + if (loanCharge.isPaid()) { + throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_PAID, loanCharge.getId()); } + final Long loanId = loanTransaction.getLoan().getId(); final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); - final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - // Charges may be waived only when the loan associated with them are // active if (!loan.getStatus().isActive()) { throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.LOAN_INACTIVE, loanCharge.getId()); } - // Validate loan charge is not already paid - if (loanCharge.isPaid()) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_PAID, loanCharge.getId()); - } - final Map changes = new LinkedHashMap<>(3); businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); - if (loanCharge.isInstalmentFee()) { - LoanInstallmentCharge chargePerInstallment; - - // final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); - if (installmentNumber != null) { - - // Get installment charge. - chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); - - if (!loanTransaction.isNotReversed()) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.ALREADY_REVERSED, - loanTransaction.getId()); - } - - // Reverse waived transaction - loanTransaction.setReversed(); - - // Get installment amount waived. - BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount(); - - // Set manually adjusted value to `1` - loanTransaction.setManuallyAdjustedOrReversed(); - - // Save updated data - this.loanTransactionRepository.saveAndFlush(loanTransaction); - - // Get installment outstanding amount - BigDecimal amountOutstandingPerInstallment = chargePerInstallment.getAmountOutstanding(); - - // Check whether the installment charge is not waived. If so throw new error - if (!chargePerInstallment.isWaived() || amountWaived == null) { - throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanChargeId); - } - - // Get loan charge total amount waived - BigDecimal totalAmountWaved = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); - - // Get loan charge outstanding amount - BigDecimal amountOutstanding = loanCharge.getAmountOutstanding(loan.getCurrency()).getAmount(); - - // Add the amount waived to outstanding amount - loanCharge.resetOutstandingAmount(amountOutstanding.add(amountWaived)); - - // Subtract the amount waived from the existing amount waived. - loanCharge.setAmountWaived(totalAmountWaved.subtract(amountWaived)); - - // Add the amount waived to the outstanding amount of the installment - chargePerInstallment.resetOutstandingAmount(amountOutstandingPerInstallment.add(amountWaived)); - - // Set the amount waived value to ZERO - chargePerInstallment.resetAmountWaived(BigDecimal.ZERO); - - // Reset waived flag - chargePerInstallment.undoWaiveFlag(); - - // Get the fee charges waived amount per installment - BigDecimal feeChargesWaivedAmount = chargePerInstallment.getInstallment().getFeeChargesWaived(loan.getCurrency()) - .getAmount(); - - // Subtract the amount waived from the existing fee charges waived amount. - chargePerInstallment.getInstallment().setFeeChargesWaived(feeChargesWaivedAmount.subtract(amountWaived)); - - // Update loan charge. - loanCharge.setInstallmentLoanCharge(chargePerInstallment, chargePerInstallment.getInstallment().getInstallmentNumber()); - - if (loanCharge.getAmount(loan.getCurrency()).compareTo(loanCharge.getAmountOutstanding(loan.getCurrency())) == 0 - && loanCharge.isWaived()) { - loanCharge.undoWaived(); - } - - this.loanChargeRepository.saveAndFlush(loanCharge); - - loan.updateLoanSummaryForUndoWaiveCharge(amountWaived); - - changes.put("amount", amountWaived); - - } else { - throw new InstallmentNotFoundException(command.entityId()); - } - } - - saveLoanWithDataIntegrityViolationChecks(loan); + undoWaivedCharge(changes, loan, loanTransaction, loanChargePaidBy); businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeUndoBusinessEvent(loanCharge)); - LoanTransaction loanTransactionData = this.loanTransactionRepository.getReferenceById(command.entityId()); - changes.put("principalPortion", loanTransactionData.getPrincipalPortion()); - changes.put("interestPortion", loanTransactionData.getInterestPortion(loan.getCurrency())); - changes.put("feeChargesPortion", loanTransactionData.getFeeChargesPortion(loan.getCurrency())); - changes.put("penaltyChargesPortion", loanTransactionData.getPenaltyChargesPortion(loan.getCurrency())); - changes.put("outstandingLoanBalance", loanTransactionData.getOutstandingLoanBalance()); - changes.put("id", loanTransactionData.getId()); - changes.put("date", loanTransactionData.getTransactionDate()); + changes.put("principalPortion", loanTransaction.getPrincipalPortion()); + changes.put("interestPortion", loanTransaction.getInterestPortion(loan.getCurrency())); + changes.put("feeChargesPortion", loanTransaction.getFeeChargesPortion(loan.getCurrency())); + changes.put("penaltyChargesPortion", loanTransaction.getPenaltyChargesPortion(loan.getCurrency())); + changes.put("outstandingLoanBalance", loanTransaction.getOutstandingLoanBalance()); + changes.put("id", loanTransaction.getId()); + changes.put("date", loanTransaction.getTransactionDate()); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // - .withEntityId(loanChargeId) // + .withEntityId(loanCharge.getId()) // .withLoanId(loanId) // .with(changes).build(); } + private void undoWaivedCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, + final LoanChargePaidBy loanChargePaidBy) { + switch (loanChargePaidBy.getLoanCharge().getChargeTimeType()) { + case SPECIFIED_DUE_DATE -> undoSpecifiedDueDateCharge(changes, loan, loanTransaction, loanChargePaidBy); + case INSTALMENT_FEE -> undoInstalmentFee(changes, loan, loanTransaction, loanChargePaidBy); + default -> throw new UnsupportedOperationException( + "Undo waive charge is not support for this charge: " + loanChargePaidBy.getLoanCharge().getChargeTimeType()); + } + } + + private void undoInstalmentFee(Map changes, Loan loan, LoanTransaction loanTransaction, + LoanChargePaidBy loanChargePaidBy) { + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); + final Integer installmentNumber = loanChargePaidBy.getInstallmentNumber(); + LoanInstallmentCharge chargePerInstallment; + // final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if (installmentNumber != null) { + // Get installment charge. + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + // Get installment amount waived. + BigDecimal amountWaived = chargePerInstallment.getAmountWaived(loan.getCurrency()).getAmount(); + // Check whether the installment charge is not waived. If so throw new error + if (!chargePerInstallment.isWaived() || amountWaived == null) { + throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); + } + // Reverse waived transaction + loanTransaction.reverse(); + // Set manually adjusted value to `1` + loanTransaction.setManuallyAdjustedOrReversed(); + // Get loan charge outstanding amount + BigDecimal amountOutstanding = loanCharge.getAmountOutstanding(loan.getCurrency()).getAmount(); + // Add the amount waived to outstanding amount + loanCharge.setOutstandingAmount(amountOutstanding.add(amountWaived)); + // Get loan charge total amount waived + BigDecimal totalAmountWaved = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); + // Subtract the amount waived from the existing amount waived. + loanCharge.setAmountWaived(totalAmountWaved.subtract(amountWaived)); + // Get installment outstanding amount + BigDecimal amountOutstandingPerInstallment = chargePerInstallment.getAmountOutstanding(); + // Add the amount waived to the outstanding amount of the installment + chargePerInstallment.setOutstandingAmount(amountOutstandingPerInstallment.add(amountWaived)); + // Set the amount waived value to ZERO + chargePerInstallment.setAmountWaived(null); + // Reset waived flag + chargePerInstallment.undoWaiveFlag(); + // Update installment balances + updateRepaymentInstalmentWithWaivedAmount(loanCharge, chargePerInstallment.getInstallment(), amountWaived); + // Update loan charge. + loanCharge.setInstallmentLoanCharge(chargePerInstallment, chargePerInstallment.getInstallment().getInstallmentNumber()); + if (loanCharge.amount().compareTo(loanCharge.amountOutstanding()) == 0 && loanCharge.isWaived()) { + loanCharge.undoWaived(); + } + loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + changes.put(AMOUNT, amountWaived); + } else { + throw new InstallmentNotFoundException(loanTransaction.getId()); + } + } + + private void undoSpecifiedDueDateCharge(final Map changes, final Loan loan, final LoanTransaction loanTransaction, + final LoanChargePaidBy loanChargePaidBy) { + + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + LoanCharge loanCharge = loanChargePaidBy.getLoanCharge(); + BigDecimal amountWaived = loanCharge.getAmountWaived(loan.getCurrency()).getAmount(); + if (!loanCharge.isWaived() || amountWaived == null) { + throw new LoanChargeWaiveCannotBeReversedException(LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); + } + loanTransaction.reverse(); + loanTransaction.setManuallyAdjustedOrReversed(); + loanCharge.setOutstandingAmount(loanCharge.amountOutstanding().add(amountWaived)); + loanCharge.setAmountWaived(null); + loanCharge.undoWaived(); + LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallments().stream() + .filter(e -> isPartOfThisInstallment(loanCharge, e)).findFirst().orElseThrow(); + updateRepaymentInstalmentWithWaivedAmount(loanCharge, installment, amountWaived); + loan.updateLoanSummaryForUndoWaiveCharge(amountWaived, loanCharge.isPenaltyCharge()); + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + changes.put(AMOUNT, amountWaived); + } + + private void updateRepaymentInstalmentWithWaivedAmount(final LoanCharge loanCharge, final LoanRepaymentScheduleInstallment installment, + final BigDecimal amountWaived) { + if (loanCharge.isPenaltyCharge()) { + // Get the penalty charges waived amount per installment + BigDecimal penaltyChargesWaivedAmount = installment.getPenaltyChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); + // Subtract the amount waived from the existing fee charges waived amount. + installment.setPenaltyChargesWaived(penaltyChargesWaivedAmount.subtract(amountWaived)); + } else { + // Get the fee charges waived amount per installment + BigDecimal feeChargesWaivedAmount = installment.getFeeChargesWaived(loanCharge.getLoan().getCurrency()).getAmount(); + // Subtract the amount waived from the existing fee charges waived amount. + installment.setFeeChargesWaived(feeChargesWaivedAmount.subtract(amountWaived)); + } + } + @Transactional @Override public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { @@ -2829,16 +2838,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private void checkClientOrGroupActive(final Loan loan) { final Client client = loan.client(); - if (client != null) { - if (client.isNotActive()) { - throw new ClientNotActiveException(client.getId()); - } + if (client != null && client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); } final Group group = loan.group(); - if (group != null) { - if (group.isNotActive()) { - throw new GroupNotActiveException(group.getId()); - } + if (group != null && group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); } } @@ -2950,7 +2955,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (diff < 1) { diff = 1L; } - LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue.intValue() + 1); + LocalDate startDate = dueDate.plusDays(penaltyWaitPeriodValue + 1L); int frequencyNumber = 1; if (feeFrequency == null) { scheduleDates.put(frequencyNumber++, startDate.minusDays(diff)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index 26273f3aa..ac4913248 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -35,6 +35,8 @@ import java.text.DateFormat; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -94,6 +96,8 @@ public class ClientLoanIntegrationTest { private SavingsAccountHelper savingsAccountHelper; private AccountTransferHelper accountTransferHelper; + private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + @BeforeEach public void setup() { Utils.initializeRESTAssured(); @@ -104,6 +108,9 @@ public class ClientLoanIntegrationTest { this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); this.schedulerJobHelper = new SchedulerJobHelper(this.requestSpec); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + this.periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(this.requestSpec, this.responseSpec); + this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); } @Test @@ -815,7 +822,7 @@ public class ClientLoanIntegrationTest { map.put("id", transId.toString()); map.put("loanId", loanID.toString()); final String putBody = gson.toJson(map); - chargeId = this.loanTransactionHelper.undoWaiveChargesForLoan(loanID, transId, putBody); + chargeId = this.loanTransactionHelper.undoWaiveChargesForLoanReturnResourceId(loanID, transId, putBody); break; } } @@ -1551,7 +1558,6 @@ public class ClientLoanIntegrationTest { */ @Test public void loanWithFlatCahargesAndCashBasedAccountingEnabled() { - this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); @@ -1739,7 +1745,6 @@ public class ClientLoanIntegrationTest { */ @Test public void loanWithCahargesOfTypeAmountPercentageAndCashBasedAccountingEnabled() { - this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec); final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec); ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID); @@ -5976,6 +5981,321 @@ public class ClientLoanIntegrationTest { assertEquals(clientCollateralId, clientCollateralIdResult); } + @Test + public void undoWaivedChargeTransactionDoesNotExist() { + ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(404).build(); + LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, responseSpec); + HashMap response = loanTransactionHelper.undoWaiveChargesForLoan(-1, -2, ""); + assertEquals("error.msg.loan.id.invalid", ((Map) ((List) response.get("errors")).get(0)).get("userMessageGlobalisationCode")); + assertEquals("Transaction with identifier -2 does not exist for loan with identifier -1.", + ((Map) ((List) response.get("errors")).get(0)).get("defaultUserMessage")); + } + + @Test + public void undoWaivedChargeWaiveTransactionDoesNotExist() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + String loanDetails = this.loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + final Integer loanTransactionId = (Integer) ((Map) ((List) JsonPath.from(loanDetails).get("transactions")).get(0)).get("id"); + + ResponseSpecification responseSpec = new ResponseSpecBuilder().expectStatusCode(403).build(); + LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, responseSpec); + HashMap response = loanTransactionHelper.undoWaiveChargesForLoan(loanID, loanTransactionId, ""); + assertEquals("error.msg.loan.transaction.undo.waive.charge", + ((Map) ((List) response.get("errors")).get(0)).get("userMessageGlobalisationCode")); + assertEquals("Transaction is not a waive charge type.", ((Map) ((List) response.get("errors")).get(0)).get("defaultUserMessage")); + } + + @Test + public void undoWaivedCharge() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", true)); + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2011"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan("02 September 2022", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September 2022", loanID, "1000"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1000.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + LocalDate targetDate = LocalDate.of(2022, 9, 7); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(penalty), penaltyCharge1AddedDate, "10")); + + this.loanTransactionHelper.noAccrualTransactionForRepayment(loanID); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(0, loanSchedule.get(1).get("totalWaivedForPeriod")); + + HashMap loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(10.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(0.0f, loanSummary.get("feeChargesCharged")); + assertEquals(0.0f, loanSummary.get("feeChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("feeChargesWaived")); + assertEquals(1010.0f, loanSummary.get("totalOutstanding")); + assertEquals(0.0f, loanSummary.get("totalWaived")); + + this.loanTransactionHelper.waiveChargesForLoan(loanID, penalty1LoanChargeId, ""); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0, loanSchedule.get(1).get("feeChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesWaived")); + assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(10.0f, loanSchedule.get(1).get("totalWaivedForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(0.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(10.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(0.0f, loanSummary.get("feeChargesCharged")); + assertEquals(0.0f, loanSummary.get("feeChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("feeChargesWaived")); + assertEquals(1000.0f, loanSummary.get("totalOutstanding")); + assertEquals(10.0f, loanSummary.get("totalWaived")); + + List transactions = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "transactions"); + assertEquals(10.0f, (float) transactions.get(1).get("amount")); + assertEquals(9, (int) ((HashMap) transactions.get(1).get("type")).get("id")); + Integer waiveTransactionId = (int) transactions.get(1).get("id"); + + this.loanTransactionHelper.undoWaiveChargesForLoan(loanID, waiveTransactionId, ""); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0, loanSchedule.get(1).get("feeChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(0.0f, loanSchedule.get(1).get("totalWaivedForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(10.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(0.0f, loanSummary.get("feeChargesCharged")); + assertEquals(0.0f, loanSummary.get("feeChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("feeChargesWaived")); + assertEquals(1010.0f, loanSummary.get("totalOutstanding")); + assertEquals(0.0f, loanSummary.get("totalWaived")); + + transactions = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "transactions"); + assertEquals(10.0f, (float) transactions.get(1).get("amount")); + assertEquals(9, (int) ((HashMap) transactions.get(1).get("type")).get("id")); + assertEquals(true, transactions.get(1).get("manuallyReversed")); + + Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer fee1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), feeCharge1AddedDate, "10")); + + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(feeCharge1AddedDate); + + transactions = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "transactions"); + assertEquals(20.0f, (float) transactions.get(2).get("amount")); + assertEquals(10, (int) ((HashMap) transactions.get(2).get("type")).get("id")); + Integer accrualTransactionId = (int) transactions.get(2).get("id"); + + List journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + accrualTransactionId); + assertEquals(10.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(incomeAccount.getAccountID(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(2).get("amount")); + assertEquals(incomeAccount.getAccountID(), (int) journalEntries.get(2).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(2).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(3).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(3).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(3).get("entryType")).get("value")); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0, loanSchedule.get(1).get("feeChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1020.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1020.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(0, loanSchedule.get(1).get("totalWaivedForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(10.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(10.0f, loanSummary.get("feeChargesCharged")); + assertEquals(10.0f, loanSummary.get("feeChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("feeChargesWaived")); + assertEquals(1020.0f, loanSummary.get("totalOutstanding")); + assertEquals(0.0f, loanSummary.get("totalWaived")); + + this.loanTransactionHelper.waiveChargesForLoan(loanID, fee1LoanChargeId, ""); + + transactions = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "transactions"); + assertEquals(10.0f, (float) transactions.get(3).get("amount")); + assertEquals(9, (int) ((HashMap) transactions.get(3).get("type")).get("id")); + Integer waive2TransactionId = (int) transactions.get(3).get("id"); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + waive2TransactionId); + assertEquals(10.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(expenseAccount.getAccountID(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0.0f, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(10.0f, loanSchedule.get(1).get("feeChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1020.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1010.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(10.0f, loanSchedule.get(1).get("totalWaivedForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(10.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(10.0f, loanSummary.get("feeChargesCharged")); + assertEquals(0.0f, loanSummary.get("feeChargesOutstanding")); + assertEquals(10.0f, loanSummary.get("feeChargesWaived")); + assertEquals(1010.0f, loanSummary.get("totalOutstanding")); + assertEquals(10.0f, loanSummary.get("totalWaived")); + + this.loanTransactionHelper.undoWaiveChargesForLoan(loanID, waive2TransactionId, ""); + + transactions = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "transactions"); + assertEquals(10.0f, (float) transactions.get(3).get("amount")); + assertEquals(9, (int) ((HashMap) transactions.get(3).get("type")).get("id")); + assertEquals(true, transactions.get(3).get("manuallyReversed")); + + journalEntries = this.journalEntryHelper.getJournalEntriesByTransactionId("L" + waive2TransactionId); + assertEquals(10.0f, (float) journalEntries.get(0).get("amount")); + assertEquals(expenseAccount.getAccountID(), (int) journalEntries.get(0).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(0).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(1).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(1).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(1).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(2).get("amount")); + assertEquals(expenseAccount.getAccountID(), (int) journalEntries.get(2).get("glAccountId")); + assertEquals("DEBIT", ((HashMap) journalEntries.get(2).get("entryType")).get("value")); + assertEquals(10.0f, (float) journalEntries.get(3).get("amount")); + assertEquals(assetAccount.getAccountID(), (int) journalEntries.get(3).get("glAccountId")); + assertEquals("CREDIT", ((HashMap) journalEntries.get(3).get("entryType")).get("value")); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(10.0f, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(10.0f, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0.0f, loanSchedule.get(1).get("feeChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesWaived")); + assertEquals(10.0f, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(1020.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(1020.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + assertEquals(0.0f, loanSchedule.get(1).get("totalWaivedForPeriod")); + + loanSummary = this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, loanID, "summary"); + assertEquals(10.0f, loanSummary.get("penaltyChargesCharged")); + assertEquals(10.0f, loanSummary.get("penaltyChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("penaltyChargesWaived")); + assertEquals(10.0f, loanSummary.get("feeChargesCharged")); + assertEquals(10.0f, loanSummary.get("feeChargesOutstanding")); + assertEquals(0.0f, loanSummary.get("feeChargesWaived")); + assertEquals(1020.0f, loanSummary.get("totalOutstanding")); + assertEquals(0.0f, loanSummary.get("totalWaived")); + } + + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID) { + LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("1").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance() + .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate("03 September 2022").withSubmittedOnDate("01 September 2022").withLoanType("individual") + .build(clientID.toString(), loanProductID.toString(), null); + return this.loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + private Integer createLoanProductWithPeriodicAccrualAccountingNoInterest(final Account... accounts) { + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("1").withNumberOfRepayments("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat() + .withAccountingRulePeriodicAccrual(accounts).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0") + .build(null); + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + private void validateIfValuesAreNotOverridden(Integer loanID, Integer loanProductID) { String loanProductDetails = this.loanTransactionHelper.getLoanProductDetails(this.requestSpec, this.responseSpec, loanProductID); String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 1719595e1..15bc47d88 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -529,7 +529,14 @@ public class LoanTransactionHelper { return (Integer) response.get("resourceId"); } - public Integer undoWaiveChargesForLoan(final Integer loanId, final Integer transactionId, final String body) { + public HashMap undoWaiveChargesForLoan(final Integer loanId, final Integer transactionId, final String body) { + log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); + final String TRANSAC_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + transactionId + "?" + + Utils.TENANT_IDENTIFIER; + return Utils.performServerPut(requestSpec, responseSpec, TRANSAC_URL, body, ""); + } + + public Integer undoWaiveChargesForLoanReturnResourceId(final Integer loanId, final Integer transactionId, final String body) { log.info("--------------------------------- UNDO WAIVE CHARGES FOR LOAN --------------------------------"); final String TRANSAC_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + transactionId + "?" + Utils.TENANT_IDENTIFIER;