FINERACT-2082: New Transaction Type - Interest Payment Waiver
This commit is contained in:
+9
@@ -842,6 +842,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder loanInterestPaymentWaiverTransaction(final Long loanId) {
|
||||
this.actionName = "INTERESTPAYMENTWAIVER";
|
||||
this.entityName = "LOAN";
|
||||
this.entityId = null;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId + "/transactions/template?command=interestpaymentwaiver";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder refundLoanCharge(final Long loanId) {
|
||||
this.actionName = "CHARGEREFUND";
|
||||
this.entityName = "LOAN";
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.infrastructure.event.business.domain.loan.transaction;
|
||||
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public class LoanTransactionInterestPaymentWaiverPostBusinessEvent extends LoanTransactionBusinessEvent {
|
||||
|
||||
private static final String TYPE = "LoanTransactionInterestPaymentWaiverPostBusinessEvent";
|
||||
|
||||
public LoanTransactionInterestPaymentWaiverPostBusinessEvent(LoanTransaction value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.infrastructure.event.business.domain.loan.transaction;
|
||||
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
|
||||
public class LoanTransactionInterestPaymentWaiverPreBusinessEvent extends LoanBusinessEvent {
|
||||
|
||||
private static final String TYPE = "LoanTransactionInterestPaymentWaiverPreBusinessEvent";
|
||||
|
||||
public LoanTransactionInterestPaymentWaiverPreBusinessEvent(Loan value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
+2
@@ -37,6 +37,7 @@ public class LoanTransactionEnumData {
|
||||
private final boolean merchantIssuedRefund;
|
||||
private final boolean payoutRefund;
|
||||
private final boolean goodwillCredit;
|
||||
private final boolean interestPaymentWaiver;
|
||||
private final boolean chargeRefund;
|
||||
private final boolean contra;
|
||||
private final boolean waiveInterest;
|
||||
@@ -88,6 +89,7 @@ public class LoanTransactionEnumData {
|
||||
this.chargeAdjustment = Long.valueOf(26).equals(this.id);
|
||||
this.chargeoff = Long.valueOf(27).equals(this.id);
|
||||
this.downPayment = Long.valueOf(28).equals(this.id);
|
||||
this.interestPaymentWaiver = Long.valueOf(31).equals(this.id);
|
||||
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
|
||||
this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
|
||||
}
|
||||
|
||||
+11
-10
@@ -1387,7 +1387,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLoanSummaryDerivedFields() {
|
||||
public void updateLoanSummaryDerivedFields() {
|
||||
if (isNotDisbursed()) {
|
||||
this.summary.zeroFields();
|
||||
this.totalOverpaid = null;
|
||||
@@ -2586,9 +2586,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
|
||||
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isMerchantIssuedRefund()
|
||||
|| repaymentTransaction.isPayoutRefund() || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment()
|
||||
|| repaymentTransaction.isDownPayment()) {
|
||||
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver()
|
||||
|| repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund()
|
||||
|| repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()) {
|
||||
|
||||
if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
|
||||
@@ -2799,7 +2800,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return loanTransactionDate;
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
|
||||
public LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) {
|
||||
return getRepaymentScheduleInstallments().stream() //
|
||||
.filter(installment -> dueDate.equals(installment.getDueDate())).findFirst() //
|
||||
.orElse(null);
|
||||
@@ -2887,7 +2888,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
processIncomeAccrualTransactionOnLoanClosure();
|
||||
}
|
||||
|
||||
private void processIncomeAccrualTransactionOnLoanClosure() {
|
||||
public void processIncomeAccrualTransactionOnLoanClosure() {
|
||||
if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
|
||||
&& this.getStatus().isClosedObligationsMet() && !isNpa() && !isChargedOff()) {
|
||||
|
||||
@@ -3161,7 +3162,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private boolean isOverPaid() {
|
||||
public boolean isOverPaid() {
|
||||
return calculateTotalOverpayment().isGreaterThanZero();
|
||||
}
|
||||
|
||||
@@ -3209,7 +3210,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getTotalRecoveredPayments();
|
||||
}
|
||||
|
||||
private MonetaryCurrency loanCurrency() {
|
||||
public MonetaryCurrency loanCurrency() {
|
||||
return this.loanRepaymentScheduleDetail.getCurrency();
|
||||
}
|
||||
|
||||
@@ -3479,7 +3480,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getStatus().isClosed() || isCancelled();
|
||||
}
|
||||
|
||||
private boolean isClosedObligationsMet() {
|
||||
public boolean isClosedObligationsMet() {
|
||||
return getStatus().isClosedObligationsMet();
|
||||
}
|
||||
|
||||
@@ -6024,7 +6025,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isForeclosure() {
|
||||
public boolean isForeclosure() {
|
||||
boolean isForeClosure = false;
|
||||
if (this.loanSubStatus != null) {
|
||||
isForeClosure = LoanSubStatus.fromInt(loanSubStatus).isForeclosed();
|
||||
|
||||
+7
-2
@@ -580,11 +580,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
|
||||
public boolean isRepaymentLikeType() {
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|
||||
|| isChargeAdjustment() || isDownPayment();
|
||||
|| isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver();
|
||||
}
|
||||
|
||||
public boolean isTypeAllowedForChargeback() {
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isDownPayment();
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isDownPayment()
|
||||
|| isInterestPaymentWaiver();
|
||||
}
|
||||
|
||||
public boolean isRepayment() {
|
||||
@@ -607,6 +608,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return LoanTransactionType.GOODWILL_CREDIT.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isInterestPaymentWaiver() {
|
||||
return LoanTransactionType.INTEREST_PAYMENT_WAIVER.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isChargeRefund() {
|
||||
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
+8
-1
@@ -63,6 +63,7 @@ public enum LoanTransactionType {
|
||||
DOWN_PAYMENT(28, "loanTransactionType.downPayment"), //
|
||||
REAGE(29, "loanTransactionType.reAge"), //
|
||||
REAMORTIZE(30, "loanTransactionType.reAmortize"), //
|
||||
INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), //
|
||||
;
|
||||
|
||||
private final Integer value;
|
||||
@@ -109,6 +110,7 @@ public enum LoanTransactionType {
|
||||
case 28 -> LoanTransactionType.DOWN_PAYMENT;
|
||||
case 29 -> LoanTransactionType.REAGE;
|
||||
case 30 -> LoanTransactionType.REAMORTIZE;
|
||||
case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER;
|
||||
default -> LoanTransactionType.INVALID;
|
||||
};
|
||||
}
|
||||
@@ -133,6 +135,10 @@ public enum LoanTransactionType {
|
||||
return this.equals(LoanTransactionType.REPAYMENT);
|
||||
}
|
||||
|
||||
public boolean isInterestPaymentWaiver() {
|
||||
return this.equals(LoanTransactionType.INTEREST_PAYMENT_WAIVER);
|
||||
}
|
||||
|
||||
public boolean isMerchantIssuedRefund() {
|
||||
return this.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND);
|
||||
}
|
||||
@@ -150,7 +156,8 @@ public enum LoanTransactionType {
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment());
|
||||
return (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund() || isDownPayment()
|
||||
|| isInterestPaymentWaiver());
|
||||
}
|
||||
|
||||
public boolean isRecoveryRepayment() {
|
||||
|
||||
+3
@@ -52,6 +52,9 @@ public interface LoanWritePlatformService {
|
||||
CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId,
|
||||
JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType);
|
||||
|
||||
@Transactional
|
||||
CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command);
|
||||
|
||||
Map<String, Object> makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand);
|
||||
|
||||
CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command);
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ public enum PaymentAllocationTransactionType {
|
||||
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT, "Charge adjustment"), //
|
||||
WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"), //
|
||||
CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT, "Charge payment"), //
|
||||
REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan");
|
||||
REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund for active loan"), //
|
||||
INTEREST_PAYMENT_WAIVER(LoanTransactionType.INTEREST_PAYMENT_WAIVER, "Interest payment waiver");
|
||||
|
||||
private final LoanTransactionType loanTransactionType;
|
||||
private final String humanReadableName;
|
||||
|
||||
+2
@@ -303,6 +303,8 @@ public final class LoanEnumerations {
|
||||
LoanTransactionType.PAYOUT_REFUND.getCode(), "Payout Refund");
|
||||
case GOODWILL_CREDIT -> new LoanTransactionEnumData(LoanTransactionType.GOODWILL_CREDIT.getValue().longValue(),
|
||||
LoanTransactionType.GOODWILL_CREDIT.getCode(), "Goodwill Credit");
|
||||
case INTEREST_PAYMENT_WAIVER -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue().longValue(),
|
||||
LoanTransactionType.INTEREST_PAYMENT_WAIVER.getCode(), "Interest Payment Waiver");
|
||||
case CHARGE_REFUND -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_REFUND.getValue().longValue(),
|
||||
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
|
||||
case CHARGEBACK -> new LoanTransactionEnumData(LoanTransactionType.CHARGEBACK.getValue().longValue(),
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
case CREDIT_BALANCE_REFUND ->
|
||||
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
|
||||
handleRepayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(), ctx.getOverpaymentHolder());
|
||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
|
||||
|
||||
+100
@@ -110,6 +110,106 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
else if (loanTransactionDTO.getTransactionType().isChargeoff()) {
|
||||
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Logic for Interest Payment Waiver
|
||||
else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()) {
|
||||
createJournalEntriesForInterestPaymentWaiver(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForInterestPaymentWaiver(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
final String currencyCode = loanDTO.getCurrencyCode();
|
||||
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
|
||||
|
||||
// transaction properties
|
||||
final String transactionId = loanTransactionDTO.getTransactionId();
|
||||
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
|
||||
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
|
||||
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
|
||||
final BigDecimal feesAmount = loanTransactionDTO.getFees();
|
||||
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
|
||||
final BigDecimal overPayment = loanTransactionDTO.getOverPayment();
|
||||
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||
final boolean isReversal = loanTransactionDTO.isReversed();
|
||||
|
||||
Map<GLAccount, BigDecimal> accountMapForCredit = new LinkedHashMap<>();
|
||||
|
||||
Map<Integer, BigDecimal> accountMapForDebit = new LinkedHashMap<>();
|
||||
|
||||
if (isMarkedAsChargeOff) {
|
||||
// ChargeOFF
|
||||
// principal payment
|
||||
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId,
|
||||
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
// interest payment
|
||||
if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId,
|
||||
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
// handle fees payment
|
||||
if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId,
|
||||
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
|
||||
// handle penalty payment
|
||||
if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
|
||||
AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
// handle overpayment
|
||||
if (overPayment != null && overPayment.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, overPayment, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
} else {
|
||||
// principal payment
|
||||
if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, principalAmount, paymentTypeId, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
// interest payment
|
||||
if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
// handle fees payment
|
||||
if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
|
||||
// handle penalty payment
|
||||
if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, penaltiesAmount, paymentTypeId,
|
||||
AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(),
|
||||
accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
// handle overpayment
|
||||
if (overPayment != null && overPayment.compareTo(BigDecimal.ZERO) > 0) {
|
||||
populateCreditDebitMaps(loanProductId, overPayment, paymentTypeId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
|
||||
AccrualAccountsForLoan.INTEREST_ON_LOANS.getValue(), accountMapForCredit, accountMapForDebit);
|
||||
}
|
||||
}
|
||||
|
||||
// create credit entries
|
||||
for (Map.Entry<GLAccount, BigDecimal> creditEntry : accountMapForCredit.entrySet()) {
|
||||
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate,
|
||||
creditEntry.getValue(), isReversal, creditEntry.getKey());
|
||||
}
|
||||
|
||||
// create debit entries
|
||||
for (Map.Entry<Integer, BigDecimal> debitEntry : accountMapForDebit.entrySet()) {
|
||||
this.helper.createDebitJournalEntryOrReversalForLoan(office, currencyCode, debitEntry.getKey().intValue(), loanProductId,
|
||||
paymentTypeId, loanId, transactionId, transactionDate, debitEntry.getValue(), isReversal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-6
@@ -111,9 +111,9 @@ public class LoanTransactionsApiResource {
|
||||
+ "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n"
|
||||
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
|
||||
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
|
||||
+ "loans/1/transactions/template?command=foreclosure" + "\n"
|
||||
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
|
||||
+ "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
|
||||
+ "loans/1/transactions/template?command=foreclosure" + "\n" + "loans/1/transactions/template?command=interestPaymentWaiver"
|
||||
+ "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
|
||||
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
|
||||
public String retrieveTransactionTemplate(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
|
||||
@@ -140,9 +140,9 @@ public class LoanTransactionsApiResource {
|
||||
+ "loans/1/transactions/template?command=disburse" + "\n" + "loans/1/transactions/template?command=disburseToSavings" + "\n"
|
||||
+ "loans/1/transactions/template?command=recoverypayment" + "\n" + "loans/1/transactions/template?command=prepayLoan" + "\n"
|
||||
+ "loans/1/transactions/template?command=refundbycash" + "\n" + "loans/1/transactions/template?command=refundbytransfer" + "\n"
|
||||
+ "loans/1/transactions/template?command=foreclosure" + "\n"
|
||||
+ "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)" + "\n"
|
||||
+ "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
|
||||
+ "loans/1/transactions/template?command=foreclosure" + "\n" + "loans/1/transactions/template?command=interestPaymentWaiver"
|
||||
+ "\n" + "loans/1/transactions/template?command=creditBalanceRefund (returned 'amount' field will have the overpaid value)"
|
||||
+ "\n" + "loans/1/transactions/template?command=charge-off" + "\n" + "loans/1/transactions/template?command=downPayment" + "\n")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanTransactionsApiResourceSwagger.GetLoansLoanIdTransactionsTemplateResponse.class))) })
|
||||
public String retrieveTransactionTemplate(
|
||||
@@ -455,6 +455,8 @@ public class LoanTransactionsApiResource {
|
||||
commandRequest = builder.loanPayoutRefundTransaction(resolvedLoanId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, "goodwillCredit")) {
|
||||
commandRequest = builder.loanGoodwillCreditTransaction(resolvedLoanId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, "interestPaymentWaiver")) {
|
||||
commandRequest = builder.loanInterestPaymentWaiverTransaction(resolvedLoanId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, "chargeRefund")) {
|
||||
commandRequest = builder.refundLoanCharge(resolvedLoanId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, "waiveinterest")) {
|
||||
@@ -521,6 +523,10 @@ public class LoanTransactionsApiResource {
|
||||
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
|
||||
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.GOODWILL_CREDIT,
|
||||
resolvedLoanId, transactionDate);
|
||||
} else if (CommandParameterUtil.is(commandParam, "interestPaymentWaiver")) {
|
||||
LocalDate transactionDate = DateUtils.getBusinessLocalDate();
|
||||
transactionData = this.loanReadPlatformService.retrieveLoanPrePaymentTemplate(LoanTransactionType.INTEREST_PAYMENT_WAIVER,
|
||||
resolvedLoanId, transactionDate);
|
||||
} else if (CommandParameterUtil.is(commandParam, "waiveinterest")) {
|
||||
transactionData = this.loanReadPlatformService.retrieveWaiveInterestDetails(resolvedLoanId);
|
||||
} else if (CommandParameterUtil.is(commandParam, "writeoff")) {
|
||||
|
||||
+6
@@ -57,6 +57,8 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMerchantIssuedRefundPostBusinessEvent;
|
||||
@@ -292,6 +294,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionPayoutRefundPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||
repaymentEvent = new LoanTransactionGoodwillCreditPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isInterestPaymentWaiver()) {
|
||||
repaymentEvent = new LoanTransactionInterestPaymentWaiverPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||
repaymentEvent = new LoanChargePaymentPreBusinessEvent(loan);
|
||||
} else if (isRecoveryRepayment) {
|
||||
@@ -313,6 +317,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionPayoutRefundPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isGoodwillCredit()) {
|
||||
repaymentEvent = new LoanTransactionGoodwillCreditPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isInterestPaymentWaiver()) {
|
||||
repaymentEvent = new LoanTransactionInterestPaymentWaiverPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isChargeRefund()) {
|
||||
repaymentEvent = new LoanChargePaymentPostBusinessEvent(transaction);
|
||||
} else if (isRecoveryRepayment) {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@CommandType(entity = "LOAN", action = "INTERESTPAYMENTWAIVER")
|
||||
public class LoanInterestPaymentWaiverCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanWritePlatformService writePlatformService;
|
||||
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.makeInterestPaymentWaiver(command);
|
||||
}
|
||||
}
|
||||
+173
-1
@@ -35,32 +35,54 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepository;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.exception.CurrencyNotFoundException;
|
||||
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
||||
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
|
||||
import org.apache.fineract.portfolio.calendar.domain.Calendar;
|
||||
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
|
||||
import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateException;
|
||||
import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
|
||||
import org.apache.fineract.portfolio.client.domain.Client;
|
||||
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
|
||||
import org.apache.fineract.portfolio.group.domain.Group;
|
||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.LoanRepaymentScheduleNotFoundException;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public final class LoanEventApiJsonValidator {
|
||||
public final class LoanTransactionValidator {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final LoanApplicationValidator fromApiJsonDeserializer;
|
||||
private final LoanRepository loanRepository;
|
||||
private final ApplicationCurrencyRepository applicationCurrencyRepository;
|
||||
private final LoanUtilService loanUtilService;
|
||||
|
||||
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
|
||||
if (!dataValidationErrors.isEmpty()) {
|
||||
@@ -562,4 +584,154 @@ public final class LoanEventApiJsonValidator {
|
||||
validatePaymentDetails(baseDataValidator, element);
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
public void validateLoanClientIsActive(final Loan loan) {
|
||||
final Client client = loan.client();
|
||||
if (client != null && client.isNotActive()) {
|
||||
throw new ClientNotActiveException(client.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanGroupIsActive(final Loan loan) {
|
||||
final Group group = loan.group();
|
||||
if (group != null && group.isNotActive()) {
|
||||
throw new GroupNotActiveException(group.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) {
|
||||
if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid";
|
||||
final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid",
|
||||
defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate,
|
||||
String reversedOrCreated) {
|
||||
for (LoanTransaction txn : loan.getLoanTransactions()) {
|
||||
if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) {
|
||||
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
|
||||
final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated
|
||||
+ " because a Later Charge Refund Exists.";
|
||||
throw new LoanChargeRefundException(errorMessage, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanDisbursementIsBeforeTransactionDate(final Loan loan, final LocalDate transactionDate) {
|
||||
if (DateUtils.isBefore(transactionDate, loan.getDisbursementDate())) {
|
||||
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
|
||||
+ loan.getDisbursementDate().toString();
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage,
|
||||
transactionDate, loan.getDisbursementDate());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTransactionShouldNotBeInTheFuture(final LocalDate transactionDate) {
|
||||
if (DateUtils.isDateInTheFuture(transactionDate)) {
|
||||
final String errorMessage = "The transaction date cannot be in the future.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanHasCurrency(final Loan loan) {
|
||||
MonetaryCurrency currency = loan.getCurrency();
|
||||
final ApplicationCurrency defaultApplicationCurrency = this.applicationCurrencyRepository.findOneByCode(currency.getCode());
|
||||
if (defaultApplicationCurrency == null) {
|
||||
throw new CurrencyNotFoundException(currency.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
public void validateClientOfficeJoiningDateIsBeforeTransactionDate(Loan loan, LocalDate transactionDate) {
|
||||
if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) {
|
||||
final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate();
|
||||
if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) {
|
||||
String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office";
|
||||
String action = "repayment.or.waiver";
|
||||
String postfix = "cannot.be.made.before.client.transfer.date";
|
||||
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateActivityNotBeforeLastTransactionDate(final Loan loan, final LocalDate activityDate) {
|
||||
if (!(loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || loan.loanProduct().isHoldGuaranteeFunds())) {
|
||||
return;
|
||||
}
|
||||
LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
|
||||
if (DateUtils.isAfter(lastTransactionDate, activityDate)) {
|
||||
String errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date";
|
||||
String action = "repayment.or.waiver";
|
||||
String postfix = "cannot.be.made.before.last.transaction.date";
|
||||
throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
|
||||
final boolean allowTransactionsOnNonWorkingDay) {
|
||||
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
|
||||
final String errorMessage = "Repayment date cannot be on a non working day";
|
||||
throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday,
|
||||
final List<Holiday> holidays) {
|
||||
if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) {
|
||||
final String errorMessage = "Repayment date cannot be on a holiday";
|
||||
throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(Loan loan) {
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||
BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) {
|
||||
final Long loanId = command.getLoanId();
|
||||
Loan loan = this.loanRepository.findById(loanId).orElseThrow(() -> new LoanNotFoundException(loanId));
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
validateNewRepaymentTransaction(command.json());
|
||||
validateTransactionShouldNotBeInTheFuture(transactionDate);
|
||||
validateLoanClientIsActive(loan);
|
||||
validateLoanHasCurrency(loan);
|
||||
validateLoanGroupIsActive(loan);
|
||||
validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan);
|
||||
validateLoanDisbursementIsBeforeTransactionDate(loan, transactionDate);
|
||||
validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(loan, transactionDate, "created");
|
||||
|
||||
validateClientOfficeJoiningDateIsBeforeTransactionDate(loan, transactionDate);
|
||||
validateActivityNotBeforeLastTransactionDate(loan, transactionDate);
|
||||
HolidayDetailDTO holidayDetailDTO = loanUtilService.constructHolidayDTO(loan.getOfficeId(), loan.getDisbursementDate());
|
||||
validateRepaymentDateIsOnHoliday(transactionDate, holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
|
||||
validateRepaymentDateIsOnNonWorkingDay(transactionDate, holidayDetailDTO.getWorkingDays(),
|
||||
holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
|
||||
validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan);
|
||||
}
|
||||
|
||||
public void validateLoanTransactionInterestPaymentWaiverAfterRecalculation(Loan loan) {
|
||||
// Payment allocation calculates the new total principal repaid, and it should be validated after recalculation
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
BigDecimal totalDisbursed = loan.getDisbursedAmount();
|
||||
BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments();
|
||||
BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted);
|
||||
if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0
|
||||
&& loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) {
|
||||
final String errorMessage = "The transaction amount cannot exceed threshold.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanTransactionAssembler {
|
||||
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
|
||||
|
||||
LoanTransaction assembleTransactionAndCalculateChanges(Loan loan, JsonCommand command, Map<String, Object> changes) {
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
|
||||
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
|
||||
changes.put("locale", command.locale());
|
||||
changes.put("dateFormat", command.dateFormat());
|
||||
changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId"));
|
||||
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
changes.put("note", noteText);
|
||||
}
|
||||
if (!txnExternalId.isEmpty()) {
|
||||
changes.put(LoanApiConstants.externalIdParameterName, txnExternalId);
|
||||
}
|
||||
|
||||
final PaymentDetail paymentDetail = paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||
final LoanTransactionType repaymentTransactionType = LoanTransactionType.INTEREST_PAYMENT_WAIVER;
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||
final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount);
|
||||
return LoanTransaction.repaymentType(repaymentTransactionType, loan.getOffice(), repaymentAmount, paymentDetail, transactionDate,
|
||||
txnExternalId, null);
|
||||
}
|
||||
}
|
||||
+11
@@ -175,6 +175,17 @@ public class LoanUtilService {
|
||||
return holidayDetailDTO;
|
||||
}
|
||||
|
||||
public HolidayDetailDTO constructHolidayDTO(final Long officeId, LocalDate localDate) {
|
||||
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
|
||||
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, localDate,
|
||||
HolidayStatusType.ACTIVE.getValue());
|
||||
final WorkingDays workingDays = this.workingDaysRepository.findOne();
|
||||
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
|
||||
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
|
||||
|
||||
return new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay);
|
||||
}
|
||||
|
||||
private FloatingRateDTO constructFloatingRateDTO(final Loan loan) {
|
||||
FloatingRateDTO floatingRateDTO = null;
|
||||
if (loan.loanProduct().isLinkedToFloatingInterestRate()) {
|
||||
|
||||
+326
-21
@@ -84,6 +84,9 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent;
|
||||
@@ -109,7 +112,9 @@ import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepositor
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferTransaction;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferType;
|
||||
import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority;
|
||||
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
|
||||
@@ -162,7 +167,10 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
|
||||
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
|
||||
@@ -180,7 +188,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
|
||||
@@ -188,6 +196,7 @@ import org.apache.fineract.portfolio.note.domain.Note;
|
||||
import org.apache.fineract.portfolio.note.domain.NoteRepository;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.data.PostDatedChecksStatus;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository;
|
||||
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler;
|
||||
@@ -203,7 +212,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final LoanEventApiJsonValidator loanEventApiJsonValidator;
|
||||
private final LoanTransactionValidator loanTransactionValidator;
|
||||
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
|
||||
private final LoanRepositoryWrapper loanRepositoryWrapper;
|
||||
private final LoanAccountDomainService loanAccountDomainService;
|
||||
@@ -247,6 +256,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
|
||||
private final ErrorHandler errorHandler;
|
||||
private final LoanDownPaymentHandlerService loanDownPaymentHandlerService;
|
||||
private final AccountTransferRepository accountTransferRepository;
|
||||
private final LoanTransactionAssembler loanTransactionAssembler;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -283,11 +294,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
final AppUser currentUser = getAppUserIfPresent();
|
||||
|
||||
this.loanEventApiJsonValidator.validateDisbursement(command.json(), isAccountTransfer);
|
||||
this.loanTransactionValidator.validateDisbursement(command.json(), isAccountTransfer);
|
||||
|
||||
if (command.parameterExists("postDatedChecks")) {
|
||||
// validate with post dated checks for the disbursement
|
||||
this.loanEventApiJsonValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
|
||||
this.loanTransactionValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
|
||||
}
|
||||
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
@@ -364,7 +375,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
|
||||
CalendarEntityType.LOANS.getValue());
|
||||
if (loan.isSyncDisbursementWithMeeting()) {
|
||||
this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
|
||||
this.loanTransactionValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
|
||||
scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), scheduleGeneratorDTO.getNumberOfdays());
|
||||
}
|
||||
|
||||
@@ -956,13 +967,307 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
chargeRefundChargeType);
|
||||
}
|
||||
|
||||
private void handleLoanOverpayment(Loan loan, LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
loan.setOverpaidOnDate(transactionDate);
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan);
|
||||
loan.setClosedOnDate(null);
|
||||
loan.setActualMaturityDate(null);
|
||||
}
|
||||
|
||||
private void handleLoanRepaymentInFull(final Loan loan, final LocalDate transactionDate,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
|
||||
boolean isAllChargesPaid = true;
|
||||
for (final LoanCharge loanCharge : loan.getCharges()) {
|
||||
if (loanCharge.isActive() && loanCharge.amount().compareTo(BigDecimal.ZERO) > 0
|
||||
&& !(loanCharge.isPaid() || loanCharge.isWaived())) {
|
||||
isAllChargesPaid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isAllChargesPaid) {
|
||||
loan.setClosedOnDate(transactionDate);
|
||||
loan.setActualMaturityDate(transactionDate);
|
||||
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan);
|
||||
} else if (LoanStatus.fromInt(loan.getLoanStatus()).isOverpaid()) {
|
||||
if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
|
||||
loan.setOverpaidOnDate(null);
|
||||
}
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
|
||||
}
|
||||
loan.processIncomeAccrualTransactionOnLoanClosure();
|
||||
}
|
||||
|
||||
private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
boolean statusChanged = false;
|
||||
boolean isOverpaid = loan.getTotalOverpaid() != null && loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
|
||||
if (isOverpaid) {
|
||||
// FIXME - kw - update account balance to negative amount.
|
||||
handleLoanOverpayment(loan, transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else if (loan.getSummary().isRepaidInFull(loan.loanCurrency())) {
|
||||
handleLoanRepaymentInFull(loan, transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
|
||||
}
|
||||
if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
|
||||
loan.setOverpaidOnDate(null);
|
||||
}
|
||||
return statusChanged;
|
||||
}
|
||||
|
||||
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
|
||||
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
|
||||
if (transferTransaction != null) {
|
||||
transferTransaction.updateToLoanTransaction(newLoanTransaction);
|
||||
this.accountTransferRepository.save(transferTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check
|
||||
public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction,
|
||||
final List<LoanTransaction> loanTransactions) {
|
||||
boolean isChronologicallyLatestRepaymentOrWaiver = true;
|
||||
|
||||
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
|
||||
for (final LoanTransaction previousTransaction : loanTransactions) {
|
||||
if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed()
|
||||
&& (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())
|
||||
|| (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate())
|
||||
&& ((loanTransaction.getId() == null && previousTransaction.getId() == null)
|
||||
|| (loanTransaction.getId() != null && (previousTransaction.getId() == null
|
||||
|| loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) {
|
||||
isChronologicallyLatestRepaymentOrWaiver = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isChronologicallyLatestRepaymentOrWaiver;
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loan loan,
|
||||
LoanTransaction newInterestPaymentWaiverTransaction) {
|
||||
LocalDate recalculateFrom = null;
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
recalculateFrom = newInterestPaymentWaiverTransaction.getTransactionDate();
|
||||
}
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null);
|
||||
|
||||
ChangedTransactionDetail changedTransactionDetail = null;
|
||||
newInterestPaymentWaiverTransaction.updateLoan(loan);
|
||||
|
||||
final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction,
|
||||
loan.getLoanTransactions());
|
||||
|
||||
if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) {
|
||||
loan.addLoanTransaction(newInterestPaymentWaiverTransaction);
|
||||
}
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessorFactory()
|
||||
.determineProcessor(loan.getTransactionProcessingStrategyCode());
|
||||
|
||||
final LoanRepaymentScheduleInstallment currentInstallment = loan
|
||||
.fetchLoanRepaymentScheduleInstallment(newInterestPaymentWaiverTransaction.getTransactionDate());
|
||||
|
||||
boolean reprocess = true;
|
||||
|
||||
if (!loan.isForeclosure() && isTransactionChronologicallyLatest
|
||||
&& DateUtils.isEqualBusinessDate(newInterestPaymentWaiverTransaction.getTransactionDate()) && currentInstallment != null
|
||||
&& currentInstallment.getTotalOutstanding(loan.getCurrency())
|
||||
.isEqualTo(newInterestPaymentWaiverTransaction.getAmount(loan.getCurrency()))) {
|
||||
reprocess = false;
|
||||
}
|
||||
|
||||
if (isTransactionChronologicallyLatest && (!reprocess || !loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled())
|
||||
&& !loan.isForeclosure()) {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(newInterestPaymentWaiverTransaction,
|
||||
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||
reprocess = false;
|
||||
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
|
||||
reprocess = true;
|
||||
} else {
|
||||
final LoanRepaymentScheduleInstallment nextInstallment = loan
|
||||
.fetchRepaymentScheduleInstallment(currentInstallment.getInstallmentNumber() + 1);
|
||||
if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(loan.getCurrency()).isGreaterThanZero()) {
|
||||
reprocess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reprocess) {
|
||||
changedTransactionDetail = reprocessChangedLoanTransactions(loan, loanRepaymentScheduleTransactionProcessor,
|
||||
scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
|
||||
doPostLoanTransactionChecks(loan, newInterestPaymentWaiverTransaction.getTransactionDate(), defaultLoanLifecycleStateMachine);
|
||||
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
}
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(loan);
|
||||
}
|
||||
/***
|
||||
* Commented since throwing exception if external id present for one of the transactions. for this need to save
|
||||
* the reversed transactions first and then new transactions.
|
||||
*/
|
||||
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) {
|
||||
|
||||
loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command);
|
||||
|
||||
final Long loanId = command.getLoanId();
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
|
||||
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestPaymentWaiverPreBusinessEvent(loan));
|
||||
|
||||
// save already existing transaction ids
|
||||
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
|
||||
|
||||
final String noteText = command.stringValueOfParameterNamed("note");
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
|
||||
LoanTransaction newInterestPaymentWaiverTransaction = loanTransactionAssembler.assembleTransactionAndCalculateChanges(loan, command,
|
||||
changes);
|
||||
|
||||
ChangedTransactionDetail changedTransactionDetail = recalculateLoanWithInterestPaymentWaiverTxn(loan,
|
||||
newInterestPaymentWaiverTransaction);
|
||||
|
||||
loanTransactionValidator.validateLoanTransactionInterestPaymentWaiverAfterRecalculation(loan);
|
||||
|
||||
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newInterestPaymentWaiverTransaction);
|
||||
|
||||
saveChangedTransactionDetail(changedTransactionDetail);
|
||||
|
||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||
|
||||
saveNote(noteText, loan, newInterestPaymentWaiverTransaction);
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
|
||||
loanAccountDomainService.recalculateAccruals(loan);
|
||||
loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
|
||||
|
||||
// disable all active standing orders linked to this loan if status changes to closed
|
||||
loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
|
||||
|
||||
updateAndSavePostDatedChecksForIndividualAccount(loan, newInterestPaymentWaiverTransaction);
|
||||
updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, newInterestPaymentWaiverTransaction);
|
||||
|
||||
// Finished Notification
|
||||
|
||||
LoanTransactionBusinessEvent transactionRepaymentEvent = new LoanTransactionInterestPaymentWaiverPostBusinessEvent(
|
||||
newInterestPaymentWaiverTransaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
|
||||
.withLoanId(loan.getId()) //
|
||||
.withEntityId(newInterestPaymentWaiverTransaction.getId()) //
|
||||
.withEntityExternalId(newInterestPaymentWaiverTransaction.getExternalId()) //
|
||||
.withOfficeId(loan.getOfficeId()) //
|
||||
.withClientId(loan.getClientId()) //
|
||||
.withGroupId(loan.getGroupId()) //
|
||||
.with(changes) //
|
||||
.build();
|
||||
}
|
||||
|
||||
private void saveNote(String noteText, Loan loan, LoanTransaction loanTransaction) {
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
final Note note = Note.loanTransactionNote(loan, loanTransaction, noteText);
|
||||
this.noteRepository.save(note);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveChangedTransactionDetail(ChangedTransactionDetail changedTransactionDetail) {
|
||||
if (changedTransactionDetail != null) {
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue());
|
||||
updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
|
||||
}
|
||||
// Trigger transaction replayed event
|
||||
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAndSavePostDatedChecksForIndividualAccount(Loan loan, LoanTransaction loanTransaction) {
|
||||
if (loan.getLoanType().isIndividualAccount()) {
|
||||
// Mark Post Dated Check as paid.
|
||||
final Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = loanTransaction
|
||||
.getLoanTransactionToRepaymentScheduleMappings();
|
||||
|
||||
if (loanTransactionToRepaymentScheduleMappings != null) {
|
||||
for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : loanTransactionToRepaymentScheduleMappings) {
|
||||
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
|
||||
.getLoanRepaymentScheduleInstallment();
|
||||
if (loanRepaymentScheduleInstallment != null) {
|
||||
final boolean isPaid = loanRepaymentScheduleInstallment.isNotFullyPaidOff();
|
||||
PostDatedChecks postDatedChecks = this.postDatedChecksRepository
|
||||
.getPendingPostDatedCheck(loanRepaymentScheduleInstallment);
|
||||
|
||||
if (postDatedChecks != null) {
|
||||
if (!isPaid) {
|
||||
postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PAID);
|
||||
} else {
|
||||
postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PENDING);
|
||||
}
|
||||
this.postDatedChecksRepository.saveAndFlush(postDatedChecks);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction loanTransaction) {
|
||||
if (loan.getLoanType().isIndividualAccount()) {
|
||||
Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
|
||||
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
|
||||
loanCollateralManagement.setLoanTransactionData(loanTransaction);
|
||||
ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement();
|
||||
|
||||
if (loan.getStatus().isClosed()) {
|
||||
loanCollateralManagement.setIsReleased(true);
|
||||
BigDecimal quantity = loanCollateralManagement.getQuantity();
|
||||
clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity));
|
||||
loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement);
|
||||
}
|
||||
}
|
||||
this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType,
|
||||
final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) {
|
||||
|
||||
this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType);
|
||||
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
|
||||
this.loanTransactionValidator.validateNewRepaymentTransaction(command.json());
|
||||
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||
@@ -1083,7 +1388,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateTransaction(command.json());
|
||||
this.loanTransactionValidator.validateTransaction(command.json());
|
||||
LoanTransaction transactionToAdjust = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
|
||||
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
|
||||
|
||||
@@ -1255,7 +1560,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult chargebackLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) {
|
||||
this.loanEventApiJsonValidator.validateChargebackTransaction(command.json());
|
||||
this.loanTransactionValidator.validateChargebackTransaction(command.json());
|
||||
|
||||
LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId())
|
||||
.orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId()));
|
||||
@@ -1369,7 +1674,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateTransaction(command.json());
|
||||
this.loanTransactionValidator.validateTransaction(command.json());
|
||||
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
|
||||
@@ -1456,7 +1761,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
public CommandProcessingResult writeOff(final Long loanId, final JsonCommand command) {
|
||||
final AppUser currentUser = getAppUserIfPresent();
|
||||
|
||||
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
|
||||
this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
|
||||
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
|
||||
@@ -1534,7 +1839,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
|
||||
this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
|
||||
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
checkClientOrGroupActive(loan);
|
||||
@@ -1643,7 +1948,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json());
|
||||
this.loanTransactionValidator.validateTransactionWithNoAmount(command.json());
|
||||
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
checkClientOrGroupActive(loan);
|
||||
@@ -1839,7 +2144,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult loanReassignment(final Long loanId, final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateUpdateOfLoanOfficer(command.json());
|
||||
this.loanTransactionValidator.validateUpdateOfLoanOfficer(command.json());
|
||||
|
||||
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
|
||||
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
|
||||
@@ -1875,7 +2180,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
public CommandProcessingResult bulkLoanReassignment(final JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateForBulkLoanReassignment(command.json());
|
||||
this.loanTransactionValidator.validateForBulkLoanReassignment(command.json());
|
||||
|
||||
final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId");
|
||||
final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId");
|
||||
@@ -2412,7 +2717,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
"Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId);
|
||||
}
|
||||
LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId);
|
||||
this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
|
||||
this.loanTransactionValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails);
|
||||
|
||||
CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan));
|
||||
@@ -2506,7 +2811,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) {
|
||||
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
|
||||
this.loanTransactionValidator.validateNewRefundTransaction(command.json());
|
||||
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
@@ -2551,7 +2856,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command) {
|
||||
this.loanEventApiJsonValidator.validateMarkAsFraudLoan(command.json());
|
||||
this.loanTransactionValidator.validateMarkAsFraudLoan(command.json());
|
||||
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
@@ -2578,7 +2883,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Transactional
|
||||
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
|
||||
|
||||
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
|
||||
this.loanTransactionValidator.validateNewRefundTransaction(command.json());
|
||||
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||
@@ -2696,7 +3001,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.transactionDateParamName, element);
|
||||
final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
|
||||
this.loanEventApiJsonValidator.validateLoanForeclosure(command.json());
|
||||
this.loanTransactionValidator.validateLoanForeclosure(command.json());
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
// Got changed to match with the rest of the APIs
|
||||
changes.put("dateFormat", command.dateFormat());
|
||||
@@ -2732,7 +3037,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Transactional
|
||||
public CommandProcessingResult chargeOff(JsonCommand command) {
|
||||
|
||||
loanEventApiJsonValidator.validateChargeOffTransaction(command.json());
|
||||
loanTransactionValidator.validateChargeOffTransaction(command.json());
|
||||
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
changes.put(LoanApiConstants.transactionDateParamName,
|
||||
@@ -2807,7 +3112,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult undoChargeOff(JsonCommand command) {
|
||||
this.loanEventApiJsonValidator.validateUndoChargeOff(command.json());
|
||||
this.loanTransactionValidator.validateUndoChargeOff(command.json());
|
||||
final Long loanId = command.getLoanId();
|
||||
final Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
|
||||
|
||||
+17
-5
@@ -43,6 +43,7 @@ import org.apache.fineract.organisation.teller.data.CashierTransactionDataValida
|
||||
import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
|
||||
import org.apache.fineract.portfolio.account.domain.AccountTransferRepository;
|
||||
import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService;
|
||||
import org.apache.fineract.portfolio.account.service.AccountNumberGenerator;
|
||||
import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService;
|
||||
@@ -87,7 +88,7 @@ import org.apache.fineract.portfolio.loanaccount.mapper.LoanTransactionRelationM
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformServiceImpl;
|
||||
@@ -121,6 +122,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformServiceJpaRepositoryImpl;
|
||||
@@ -253,6 +255,14 @@ public class LoanAccountConfiguration {
|
||||
calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanTransactionAssembler.class)
|
||||
public LoanTransactionAssembler loanTransactionAssembler(ExternalIdFactory externalIdFactory,
|
||||
PaymentDetailWritePlatformService paymentDetailWritePlatformService) {
|
||||
|
||||
return new LoanTransactionAssembler(externalIdFactory, paymentDetailWritePlatformService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanCalculateRepaymentPastDueService.class)
|
||||
public LoanCalculateRepaymentPastDueService loanCalculateRepaymentPastDueService() {
|
||||
@@ -362,7 +372,7 @@ public class LoanAccountConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanWritePlatformService.class)
|
||||
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
|
||||
LoanEventApiJsonValidator loanEventApiJsonValidator,
|
||||
LoanTransactionValidator loanTransactionValidator,
|
||||
LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper,
|
||||
LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository,
|
||||
LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository,
|
||||
@@ -386,8 +396,9 @@ public class LoanAccountConfiguration {
|
||||
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccountLockService loanAccountLockService,
|
||||
ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService,
|
||||
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
|
||||
LoanDownPaymentHandlerService loanDownPaymentHandlerService) {
|
||||
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanEventApiJsonValidator, loanUpdateCommandFromApiJsonDeserializer,
|
||||
LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
|
||||
LoanTransactionAssembler loanTransactionAssembler) {
|
||||
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
|
||||
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
|
||||
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
|
||||
paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository,
|
||||
@@ -398,7 +409,8 @@ public class LoanAccountConfiguration {
|
||||
transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository,
|
||||
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
|
||||
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
|
||||
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService);
|
||||
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
|
||||
loanTransactionAssembler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -160,4 +160,5 @@
|
||||
<include file="parts/0138_add_external_event_for_loan_reaging_reamortization_2.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0139_add_disburse_without_auto_payment_command.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0140_trial_balance_with_asset_transfer_update.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0141_add_interest_payment_waiver_transaction_type.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?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.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_loan" />
|
||||
<column name="code" value="INTERESTPAYMENTWAIVER_LOAN" />
|
||||
<column name="entity_name" value="LOAN" />
|
||||
<column name="action_name" value="INTERESTPAYMENTWAIVER" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanTransactionInterestPaymentWaiverPostBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanTransactionInterestPaymentWaiverPreBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="r_enum_value">
|
||||
<column name="enum_name" value="transaction_type_enum"/>
|
||||
<column name="enum_id" valueNumeric="31"/>
|
||||
<column name="enum_message_property" value="Interest Payment Waiver"/>
|
||||
<column name="enum_value" value="Interest Payment Waiver"/>
|
||||
<column name="enum_type" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+4
-2
@@ -101,7 +101,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
|
||||
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
|
||||
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent");
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -184,7 +185,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent",
|
||||
"LoanReAgeTransactionBusinessEvent", "LoanUndoReAgeTransactionBusinessEvent", "LoanReAmortizeTransactionBusinessEvent",
|
||||
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent");
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
+9
@@ -138,6 +138,7 @@ public abstract class BaseLoanIntegrationTest {
|
||||
protected final Account chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
|
||||
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
|
||||
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
|
||||
protected final Account goodwillIncomeAccount = accountHelper.createIncomeAccount();
|
||||
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
|
||||
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
|
||||
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
|
||||
@@ -710,6 +711,14 @@ public abstract class BaseLoanIntegrationTest {
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
protected Long addInterestPaymentWaiverForLoan(Long loanId, Double amount, String date) {
|
||||
String firstRepaymentUUID = UUID.randomUUID().toString();
|
||||
PostLoansLoanIdTransactionsResponse response = loanTransactionHelper.makeInterestPaymentWaiver(loanId,
|
||||
new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en")
|
||||
.transactionAmount(amount).externalId(firstRepaymentUUID));
|
||||
return response.getResourceId();
|
||||
}
|
||||
|
||||
protected Long chargeOffLoan(Long loanId, String date) {
|
||||
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6) + Utils.randomStringGenerator("is", 5);
|
||||
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);
|
||||
|
||||
+69
@@ -235,6 +235,62 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
|
||||
loanProductError.get(0).get("defaultUserMessage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndReadLoanProductWithAdvancedPaymentAndInterestPaymentWaiverTransaction() {
|
||||
// given
|
||||
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
|
||||
AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation();
|
||||
|
||||
// when
|
||||
Integer loanProductId = LOAN_TRANSACTION_HELPER
|
||||
.getLoanProductId(createLoanJSON(defaultAllocation, interestPaymentWaiverAllocation));
|
||||
Assertions.assertNotNull(loanProductId);
|
||||
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
|
||||
|
||||
// then
|
||||
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
|
||||
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
|
||||
Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
|
||||
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
|
||||
Assertions.assertTrue(first.isPresent());
|
||||
Assertions.assertEquals(defaultAllocation, first.get());
|
||||
|
||||
Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
|
||||
.filter(advancedPaymentData -> "INTEREST_PAYMENT_WAIVER".equals(advancedPaymentData.getTransactionType())).findFirst();
|
||||
Assertions.assertTrue(second.isPresent());
|
||||
Assertions.assertEquals(interestPaymentWaiverAllocation, second.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateLoanProductInterestPaymentWaiverAllocationIsAdded() {
|
||||
// given a loan with one allocation
|
||||
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
|
||||
AdvancedPaymentData interestPaymentWaiverAllocation = createInterestPaymentWaiverAllocation();
|
||||
Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(createLoanJSON(defaultAllocation));
|
||||
Assertions.assertNotNull(loanProductId);
|
||||
GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
|
||||
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
|
||||
Assertions.assertEquals(1, loanProduct.getPaymentAllocation().size());
|
||||
|
||||
// when a new allocation is added
|
||||
LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(),
|
||||
updateLoanProductRequest(defaultAllocation, interestPaymentWaiverAllocation));
|
||||
|
||||
// then it shall be added.
|
||||
loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId);
|
||||
Assertions.assertNotNull(loanProduct.getPaymentAllocation());
|
||||
Assertions.assertEquals(2, loanProduct.getPaymentAllocation().size());
|
||||
Optional<AdvancedPaymentData> first = loanProduct.getPaymentAllocation().stream()
|
||||
.filter(advancedPaymentData -> "DEFAULT".equals(advancedPaymentData.getTransactionType())).findFirst();
|
||||
Assertions.assertTrue(first.isPresent());
|
||||
Assertions.assertEquals(defaultAllocation, first.get());
|
||||
|
||||
Optional<AdvancedPaymentData> second = loanProduct.getPaymentAllocation().stream()
|
||||
.filter(advancedPaymentData -> "INTEREST_PAYMENT_WAIVER".equals(advancedPaymentData.getTransactionType())).findFirst();
|
||||
Assertions.assertTrue(second.isPresent());
|
||||
Assertions.assertEquals(interestPaymentWaiverAllocation, second.get());
|
||||
}
|
||||
|
||||
private String createLoanJSON(AdvancedPaymentData... advancedPaymentData) {
|
||||
return new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4").withRepaymentAfterEvery("1")
|
||||
.withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
|
||||
@@ -267,7 +323,20 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
|
||||
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
|
||||
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
|
||||
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
|
||||
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
|
||||
return advancedPaymentData;
|
||||
}
|
||||
|
||||
private AdvancedPaymentData createInterestPaymentWaiverAllocation() {
|
||||
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
|
||||
advancedPaymentData.setTransactionType("INTEREST_PAYMENT_WAIVER");
|
||||
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
|
||||
|
||||
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_FEE,
|
||||
PaymentAllocationType.PAST_DUE_PENALTY, PaymentAllocationType.PAST_DUE_INTEREST, PaymentAllocationType.PAST_DUE_PRINCIPAL,
|
||||
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
|
||||
PaymentAllocationType.DUE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
|
||||
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
|
||||
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
|
||||
return advancedPaymentData;
|
||||
}
|
||||
|
||||
+1767
File diff suppressed because it is too large
Load Diff
+17
@@ -917,6 +917,23 @@ public final class BatchHelper {
|
||||
return createTransactionRequest(requestId, reference, "goodwillCredit", amount, LocalDate.now(Utils.getZoneIdOfTenant()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a {@link CreateTransactionLoanCommandStrategy} request with given request ID for payment
|
||||
* waiver transaction.
|
||||
*
|
||||
*
|
||||
* @param requestId
|
||||
* the request ID
|
||||
* @param reference
|
||||
* the reference
|
||||
* @param amount
|
||||
* the amount
|
||||
* @return BatchRequest the created {@link BatchRequest}
|
||||
*/
|
||||
public static BatchRequest interestPaymentWaiverRequest(final Long requestId, final Long reference, final String amount) {
|
||||
return createTransactionRequest(requestId, reference, "interestPaymentWaiver", amount, LocalDate.now(Utils.getZoneIdOfTenant()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a {@link CreateTransactionLoanCommandStrategy} request with given request ID for merchant
|
||||
* issued refund transaction.
|
||||
|
||||
+10
@@ -540,6 +540,16 @@ public class ExternalEventConfigurationHelper {
|
||||
loanUndoReAmortizeBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanUndoReAmortizeBusinessEvent);
|
||||
|
||||
Map<String, Object> loanTransactionInterestPaymentWaiverPostBusinessEvent = new HashMap<>();
|
||||
loanTransactionInterestPaymentWaiverPostBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPostBusinessEvent");
|
||||
loanTransactionInterestPaymentWaiverPostBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanTransactionInterestPaymentWaiverPostBusinessEvent);
|
||||
|
||||
Map<String, Object> loanTransactionInterestPaymentWaiverPreBusinessEvent = new HashMap<>();
|
||||
loanTransactionInterestPaymentWaiverPreBusinessEvent.put("type", "LoanTransactionInterestPaymentWaiverPreBusinessEvent");
|
||||
loanTransactionInterestPaymentWaiverPreBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanTransactionInterestPaymentWaiverPreBusinessEvent);
|
||||
|
||||
return defaults;
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -601,6 +601,16 @@ public class LoanTransactionHelper extends IntegrationTest {
|
||||
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment"));
|
||||
}
|
||||
|
||||
public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final Long loanId,
|
||||
final PostLoansLoanIdTransactionsRequest request) {
|
||||
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "interestPaymentWaiver"));
|
||||
}
|
||||
|
||||
public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final String loanExternalId,
|
||||
final PostLoansLoanIdTransactionsRequest request) {
|
||||
return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "interestPaymentWaiver"));
|
||||
}
|
||||
|
||||
public PostLoansLoanIdTransactionsResponse reAge(final Long loanId, final PostLoansLoanIdTransactionsRequest request) {
|
||||
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "reAge"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user