Credit Balance Refund loan transaction
This commit is contained in:
committed by
Arnold Gálovics
parent
3d3e8c4083
commit
d1168833cc
+24
@@ -80,6 +80,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/** Logic for Credit Balance Refunds **/
|
||||
else if (loanTransactionDTO.getTransactionType().isCreditBalanceRefund()) {
|
||||
createJournalEntriesForCreditBalanceRefund(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/** Handle Write Offs, waivers and their reversals **/
|
||||
else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest()
|
||||
|| loanTransactionDTO.getTransactionType().isWaiveCharges())) {
|
||||
@@ -397,6 +402,25 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForCreditBalanceRefund(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
// loan properties
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
final String currencyCode = loanDTO.getCurrencyCode();
|
||||
|
||||
// transaction properties
|
||||
final String transactionId = loanTransactionDTO.getTransactionId();
|
||||
final Date transactionDate = loanTransactionDTO.getTransactionDate();
|
||||
final BigDecimal refundAmount = loanTransactionDTO.getAmount();
|
||||
final boolean isReversal = loanTransactionDTO.isReversed();
|
||||
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||
|
||||
this.helper.createAccrualBasedJournalEntriesAndReversalsForLoan(office, currencyCode,
|
||||
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), AccrualAccountsForLoan.OVERPAYMENT.getValue(), loanProductId,
|
||||
paymentTypeId, loanId, transactionId, transactionDate, refundAmount, isReversal);
|
||||
}
|
||||
|
||||
private void createJournalEntriesForRefundForActiveLoan(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, Office office) {
|
||||
// TODO Auto-generated method stub
|
||||
// loan properties
|
||||
|
||||
+9
@@ -878,6 +878,15 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder creditBalanceRefund(final Long loanId) {
|
||||
this.actionName = "CREDITBALANCEREFUND";
|
||||
this.entityName = "LOAN";
|
||||
this.entityId = null;
|
||||
this.loanId = loanId;
|
||||
this.href = "/loans/" + loanId + "/transactions?command=creditBalanceRefund";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder undoWaiveChargeTransaction(final Long loanId, final Long transactionId) {
|
||||
this.actionName = "UNDO";
|
||||
this.entityName = "WAIVECHARGE";
|
||||
|
||||
+1
@@ -55,6 +55,7 @@ public class BusinessEventNotificationConstants {
|
||||
LOAN_INTEREST_RECALCULATION("loan_interest_recalculation"), //
|
||||
LOAN_REFUND("loan_refund"), //
|
||||
LOAN_FORECLOSURE("loan_foreclosure"), //
|
||||
LOAN_CREDIT_BALANCE_REFUND("loan_credit_balance_refund"), //
|
||||
LOAN_CREATE("loan_create"), //
|
||||
LOAN_PRODUCT_CREATE("loan_product_create"), //
|
||||
SAVINGS_ACTIVATE("savings_activated"), //
|
||||
|
||||
+5
@@ -164,6 +164,8 @@ public class LoanTransactionsApiResource {
|
||||
DateUtils.getDateTimeZoneOfTenant());
|
||||
}
|
||||
transactionData = this.loanReadPlatformService.retrieveLoanForeclosureTemplate(loanId, transactionDate);
|
||||
} else if (is(commandParam, "creditBalanceRefund")) {
|
||||
transactionData = this.loanReadPlatformService.retrieveCreditBalanceRefundTemplate(loanId);
|
||||
} else {
|
||||
throw new UnrecognizedQueryParamException("command", commandParam);
|
||||
}
|
||||
@@ -239,6 +241,9 @@ public class LoanTransactionsApiResource {
|
||||
} else if (is(commandParam, "foreclosure")) {
|
||||
final CommandWrapper commandRequest = builder.loanForeclosure(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
} else if (is(commandParam, "creditBalanceRefund")) {
|
||||
final CommandWrapper commandRequest = builder.creditBalanceRefund(loanId).build();
|
||||
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
|
||||
+6
@@ -44,6 +44,7 @@ public class LoanTransactionEnumData {
|
||||
private final boolean chargePayment;
|
||||
private final boolean refund;
|
||||
private final boolean refundForActiveLoans;
|
||||
private final boolean creditBalanceRefund;
|
||||
|
||||
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
||||
this.id = id;
|
||||
@@ -65,6 +66,7 @@ public class LoanTransactionEnumData {
|
||||
this.refund = Long.valueOf(16).equals(this.id);
|
||||
this.chargePayment = Long.valueOf(17).equals(this.id);
|
||||
this.refundForActiveLoans = Long.valueOf(18).equals(this.id);
|
||||
this.creditBalanceRefund = Long.valueOf(20).equals(this.id);
|
||||
}
|
||||
|
||||
public Long id() {
|
||||
@@ -152,4 +154,8 @@ public class LoanTransactionEnumData {
|
||||
return this.refundForActiveLoans;
|
||||
}
|
||||
|
||||
public boolean isCreditBalanceRefund() {
|
||||
return this.creditBalanceRefund;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
@@ -106,6 +106,8 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
|
||||
break;
|
||||
case WRITE_OFF_OUTSTANDING_UNDO:
|
||||
break;
|
||||
case LOAN_CREDIT_BALANCE_REFUND:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+52
-6
@@ -3615,10 +3615,11 @@ public class Loan extends AbstractPersistableCustom {
|
||||
validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
|
||||
transactionForAdjustment.getTransactionDate());
|
||||
|
||||
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) {
|
||||
final String errorMessage = "Only transactions of type repayment or waiver can be adjusted.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction", "adjustment.is.only.allowed.to.repayment.or.waiver.transaction",
|
||||
errorMessage);
|
||||
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()
|
||||
&& transactionForAdjustment.isNotCreditBalanceRefund()) {
|
||||
final String errorMessage = "Only transactions of type repayment, waiver or credit balance refund can be adjusted.";
|
||||
throw new InvalidLoanTransactionTypeException("transaction",
|
||||
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage);
|
||||
}
|
||||
|
||||
transactionForAdjustment.reverse();
|
||||
@@ -3698,7 +3699,8 @@ public class Loan extends AbstractPersistableCustom {
|
||||
}
|
||||
|
||||
for (final LoanTransaction loanTransaction : this.loanTransactions) {
|
||||
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan()) && !loanTransaction.isReversed()) {
|
||||
if ((loanTransaction.isRefund() || loanTransaction.isRefundForActiveLoan() || loanTransaction.isCreditBalanceRefund())
|
||||
&& !loanTransaction.isReversed()) {
|
||||
totalPaidInRepayments = totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
|
||||
}
|
||||
}
|
||||
@@ -5157,6 +5159,14 @@ public class Loan extends AbstractPersistableCustom {
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
break;
|
||||
case LOAN_CREDIT_BALANCE_REFUND:
|
||||
if (!status().isOverpaid()) {
|
||||
final String defaultUserMessage = "Loan Credit Balance Refund is not allowed. Loan Account is not Overpaid.";
|
||||
final ApiParameterError error = ApiParameterError
|
||||
.generalError("error.msg.loan.credit.balance.refund.account.is.not.overpaid", defaultUserMessage);
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -6062,6 +6072,40 @@ public class Loan extends AbstractPersistableCustom {
|
||||
return this.guaranteeAmountDerived == null ? BigDecimal.ZERO : this.guaranteeAmountDerived;
|
||||
}
|
||||
|
||||
public void creditBalanceRefund(LoanTransaction newCreditBalanceRefundTransaction,
|
||||
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, List<Long> existingTransactionIds,
|
||||
List<Long> existingReversedTransactionIds) {
|
||||
validateAccountStatus(LoanEvent.LOAN_CREDIT_BALANCE_REFUND);
|
||||
|
||||
validateRefundDateIsAfterLastRepayment(newCreditBalanceRefundTransaction.getTransactionDate());
|
||||
|
||||
if (!newCreditBalanceRefundTransaction.isGreaterThanZeroAndLessThanOrEqualTo(this.totalOverpaid)) {
|
||||
final String errorMessage = "Transaction Amount ("
|
||||
+ newCreditBalanceRefundTransaction.getAmount(getCurrency()).getAmount().toString()
|
||||
+ ") must be > zero and <= Overpaid amount (" + this.totalOverpaid.toString() + ").";
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
final ApiParameterError error = ApiParameterError.parameterError(
|
||||
"error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount", errorMessage, "transactionAmount",
|
||||
newCreditBalanceRefundTransaction.getAmount(getCurrency()));
|
||||
dataValidationErrors.add(error);
|
||||
|
||||
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
|
||||
dataValidationErrors);
|
||||
}
|
||||
|
||||
existingTransactionIds.addAll(findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
|
||||
|
||||
this.loanTransactions.add(newCreditBalanceRefundTransaction);
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
this.loanStatus = LoanStatus.CLOSED_OBLIGATIONS_MET.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction loanTransaction,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
|
||||
final List<Long> existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List<Holiday> holidays,
|
||||
@@ -6179,7 +6223,8 @@ public class Loan extends AbstractPersistableCustom {
|
||||
|
||||
LocalDate lastTransactionDate = null;
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
if ((transaction.isRepayment() || transaction.isRefundForActiveLoan()) && transaction.isNonZero()) {
|
||||
if ((transaction.isRepayment() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund())
|
||||
&& transaction.isNonZero() && transaction.isNotReversed()) {
|
||||
lastTransactionDate = transaction.getTransactionDate();
|
||||
}
|
||||
}
|
||||
@@ -6841,4 +6886,5 @@ public class Loan extends AbstractPersistableCustom {
|
||||
public void adjustNetDisbursalAmount(BigDecimal adjustedAmount) {
|
||||
this.netDisbursalAmount = adjustedAmount.subtract(this.deriveSumTotalOfChargesDueAtDisbursement());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -78,4 +78,7 @@ public interface LoanAccountDomainService {
|
||||
void disableStandingInstructionsLinkedToClosedLoan(Loan loan);
|
||||
|
||||
void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened);
|
||||
|
||||
CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount,
|
||||
String noteText, String externalId);
|
||||
}
|
||||
|
||||
+36
@@ -623,6 +623,41 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResultBuilder creditBalanceRefund(Long loanId, LocalDate transactionDate, BigDecimal transactionAmount,
|
||||
String noteText, String externalId) {
|
||||
final Loan loan = this.loanAccountAssembler.assembleFrom(loanId);
|
||||
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BusinessEvents.LOAN_CREDIT_BALANCE_REFUND,
|
||||
constructEntityMap(BusinessEntity.LOAN, loan));
|
||||
final List<Long> existingTransactionIds = new ArrayList<>();
|
||||
final List<Long> existingReversedTransactionIds = new ArrayList<>();
|
||||
AppUser currentUser = getAppUserIfPresent();
|
||||
|
||||
final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount);
|
||||
final LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount,
|
||||
transactionDate, externalId, DateUtils.getLocalDateTimeOfTenant(), currentUser);
|
||||
|
||||
loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds,
|
||||
existingReversedTransactionIds);
|
||||
|
||||
this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
|
||||
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
final Note note = Note.loanTransactionNote(loan, newCreditBalanceRefundTransaction, noteText);
|
||||
this.noteRepository.save(note);
|
||||
}
|
||||
|
||||
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
|
||||
recalculateAccruals(loan);
|
||||
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_CREDIT_BALANCE_REFUND,
|
||||
constructEntityMap(BusinessEntity.LOAN_TRANSACTION, newCreditBalanceRefundTransaction));
|
||||
|
||||
return new CommandProcessingResultBuilder().withEntityId(newCreditBalanceRefundTransaction.getId()) //
|
||||
.withOfficeId(loan.getOfficeId()) //
|
||||
.withClientId(loan.getClientId()) //
|
||||
.withGroupId(loan.getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) {
|
||||
@@ -794,4 +829,5 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -43,5 +43,6 @@ public enum LoanEvent {
|
||||
LOAN_CLOSED, //
|
||||
LOAN_EDIT_MULTI_DISBURSE_DATE, //
|
||||
LOAN_REFUND, //
|
||||
LOAN_FORECLOSURE;
|
||||
LOAN_FORECLOSURE, //
|
||||
LOAN_CREDIT_BALANCE_REFUND;
|
||||
}
|
||||
|
||||
+19
@@ -297,6 +297,13 @@ public class LoanTransaction extends AbstractPersistableCustom {
|
||||
return applyCharge;
|
||||
}
|
||||
|
||||
public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
|
||||
final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
|
||||
final PaymentDetail paymentDetail = null;
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentDetail, amount.getAmount(), paymentDate,
|
||||
externalId, createdDate, appUser);
|
||||
}
|
||||
|
||||
public static LoanTransaction refundForActiveLoan(final Office office, final Money amount, final PaymentDetail paymentDetail,
|
||||
final LocalDate paymentDate, final String externalId, final LocalDateTime createdDate, final AppUser appUser) {
|
||||
return new LoanTransaction(null, office, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentDetail, amount.getAmount(), paymentDate,
|
||||
@@ -573,6 +580,10 @@ public class LoanTransaction extends AbstractPersistableCustom {
|
||||
return !isInterestWaiver() && !isChargesWaiver();
|
||||
}
|
||||
|
||||
public boolean isNotCreditBalanceRefund() {
|
||||
return !isCreditBalanceRefund();
|
||||
}
|
||||
|
||||
public boolean isChargePayment() {
|
||||
return getTypeOf().isChargePayment() && isNotReversed();
|
||||
}
|
||||
@@ -616,6 +627,10 @@ public class LoanTransaction extends AbstractPersistableCustom {
|
||||
return getAmount(currency).isGreaterThanZero();
|
||||
}
|
||||
|
||||
public boolean isGreaterThanZeroAndLessThanOrEqualTo(BigDecimal totalOverpaid) {
|
||||
return isNonZero() && this.amount.compareTo(totalOverpaid) <= 0;
|
||||
}
|
||||
|
||||
public boolean isNotZero(final MonetaryCurrency currency) {
|
||||
return !getAmount(currency).isZero();
|
||||
}
|
||||
@@ -700,6 +715,10 @@ public class LoanTransaction extends AbstractPersistableCustom {
|
||||
return LoanTransactionType.REFUND.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isCreditBalanceRefund() {
|
||||
return LoanTransactionType.CREDIT_BALANCE_REFUND.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public void updateExternalId(final String externalId) {
|
||||
this.externalId = externalId;
|
||||
}
|
||||
|
||||
+5
-1
@@ -51,7 +51,8 @@ public enum LoanTransactionType {
|
||||
REFUND(16, "loanTransactionType.refund"), //
|
||||
CHARGE_PAYMENT(17, "loanTransactionType.chargePayment"), //
|
||||
REFUND_FOR_ACTIVE_LOAN(18, "loanTransactionType.refund"), //
|
||||
INCOME_POSTING(19, "loanTransactionType.incomePosting");
|
||||
INCOME_POSTING(19, "loanTransactionType.incomePosting"), //
|
||||
CREDIT_BALANCE_REFUND(20, "loanTransactionType.creditBalanceRefund");
|
||||
|
||||
private final Integer value;
|
||||
private final String code;
|
||||
@@ -131,6 +132,9 @@ public enum LoanTransactionType {
|
||||
case 19:
|
||||
loanTransactionType = LoanTransactionType.INCOME_POSTING;
|
||||
break;
|
||||
case 20:
|
||||
loanTransactionType = LoanTransactionType.CREDIT_BALANCE_REFUND;
|
||||
break;
|
||||
default:
|
||||
loanTransactionType = LoanTransactionType.INVALID;
|
||||
break;
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@CommandType(entity = "LOAN", action = "CREDITBALANCEREFUND")
|
||||
public class CreditBalanceRefundCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
|
||||
try {
|
||||
return this.writePlatformService.creditBalanceRefund(command.getLoanId(), command);
|
||||
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
|
||||
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.creditBalanceRefund", "Credit Balance Refund");
|
||||
return CommandProcessingResult.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve, final String msgType,
|
||||
final String msgDescription) {
|
||||
|
||||
if (realCause.getMessage().contains("external_id")) {
|
||||
|
||||
final String externalId = command.stringValueOfParameterNamed("externalId");
|
||||
throw new PlatformDataIntegrityException("error.msg." + msgType + ".duplicate.externalId",
|
||||
msgDescription + " with externalId `" + externalId + "` already exists", "externalId", externalId);
|
||||
}
|
||||
|
||||
logAsErrorUnexpectedDataIntegrityException(dve);
|
||||
throw new PlatformDataIntegrityException("error.msg.loan.charge.unknown.data.integrity.issue",
|
||||
"Unknown data integrity issue with resource.");
|
||||
}
|
||||
|
||||
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
|
||||
log.error("Error occured.", dve);
|
||||
}
|
||||
}
|
||||
+3
@@ -529,6 +529,9 @@ public final class LoanEventApiJsonValidator {
|
||||
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
|
||||
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
|
||||
|
||||
final String externalId = this.fromApiJsonHelper.extractStringNamed("externalId", element);
|
||||
baseDataValidator.reset().parameter("externalId").value(externalId).notExceedingLengthOf(100);
|
||||
|
||||
validatePaymentDetails(baseDataValidator, element);
|
||||
throwExceptionIfValidationWarningsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
+2
@@ -128,6 +128,8 @@ public interface LoanReadPlatformService {
|
||||
|
||||
LoanTransactionData retrieveRefundByCashTemplate(Long loanId);
|
||||
|
||||
LoanTransactionData retrieveCreditBalanceRefundTemplate(Long loanId);
|
||||
|
||||
Collection<InterestRatePeriodData> retrieveLoanInterestRatePeriodData(LoanAccountData loan);
|
||||
|
||||
Collection<Long> retrieveLoanIdsWithPendingIncomePostingTransactions();
|
||||
|
||||
+24
-10
@@ -2118,24 +2118,38 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
|
||||
|
||||
@Override
|
||||
public LoanTransactionData retrieveRefundByCashTemplate(Long loanId) {
|
||||
// TODO Auto-generated method stub
|
||||
this.context.authenticatedUser();
|
||||
|
||||
// TODO - KW - OPTIMIZE - write simple sql query to fetch back date of
|
||||
// possible next transaction date.
|
||||
final Collection<PaymentTypeData> paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||
final MonetaryCurrency currency = loan.getCurrency();
|
||||
return retrieveRefundTemplate(loanId, LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, paymentOptions, loan.getCurrency(),
|
||||
retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), loan.getNetDisbursalAmount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanTransactionData retrieveCreditBalanceRefundTemplate(Long loanId) {
|
||||
this.context.authenticatedUser();
|
||||
|
||||
final Collection<PaymentTypeData> paymentOptions = null;
|
||||
final BigDecimal netDisbursal = null;
|
||||
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||
return retrieveRefundTemplate(loanId, LoanTransactionType.CREDIT_BALANCE_REFUND, paymentOptions, loan.getCurrency(),
|
||||
loan.getTotalOverpaid(), netDisbursal);
|
||||
|
||||
}
|
||||
|
||||
private LoanTransactionData retrieveRefundTemplate(Long loanId, LoanTransactionType loanTransactionType,
|
||||
Collection<PaymentTypeData> paymentOptions, MonetaryCurrency currency, BigDecimal transactionAmount, BigDecimal netDisbursal) {
|
||||
|
||||
final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
|
||||
|
||||
final CurrencyData currencyData = applicationCurrency.toData();
|
||||
|
||||
final LocalDate earliestUnpaidInstallmentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final LocalDate currentDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN);
|
||||
final Collection<PaymentTypeData> paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
|
||||
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, earliestUnpaidInstallmentDate,
|
||||
retrieveTotalPaidInAdvance(loan.getId()).getPaidInAdvance(), null, loan.getNetDisbursalAmount(), null, null, null, null,
|
||||
null, paymentOptions, null, null, null, null, false);
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(loanTransactionType);
|
||||
return new LoanTransactionData(null, null, null, transactionType, null, currencyData, currentDate, transactionAmount, null,
|
||||
netDisbursal, null, null, null, null, null, paymentOptions, null, null, null, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
@@ -116,4 +116,6 @@ public interface LoanWritePlatformService {
|
||||
|
||||
CommandProcessingResult makeGLIMLoanRepayment(Long loanId, JsonCommand command);
|
||||
|
||||
CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command);
|
||||
|
||||
}
|
||||
|
||||
+32
-1
@@ -3213,10 +3213,40 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
return installments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) {
|
||||
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
|
||||
|
||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||
final String noteText = command.stringValueOfParameterNamedAllowingNull("note");
|
||||
final String externalId = command.stringValueOfParameterNamedAllowingNull("externalId");
|
||||
|
||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||
changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate"));
|
||||
changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount"));
|
||||
changes.put("locale", command.locale());
|
||||
changes.put("dateFormat", command.dateFormat());
|
||||
|
||||
if (StringUtils.isNotBlank(noteText)) {
|
||||
changes.put("note", noteText);
|
||||
}
|
||||
if (StringUtils.isNotBlank(externalId)) {
|
||||
changes.put("externalId", externalId);
|
||||
}
|
||||
|
||||
final CommandProcessingResultBuilder commandProcessingResultBuilder = this.loanAccountDomainService.creditBalanceRefund(loanId,
|
||||
transactionDate, transactionAmount, noteText, externalId);
|
||||
|
||||
return commandProcessingResultBuilder //
|
||||
.withCommandId(command.commandId()).with(changes) //
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json());
|
||||
|
||||
@@ -3400,4 +3430,5 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -411,6 +411,10 @@ public final class LoanEnumerations {
|
||||
optionData = new LoanTransactionEnumData(LoanTransactionType.INCOME_POSTING.getValue().longValue(),
|
||||
LoanTransactionType.INCOME_POSTING.getCode(), "Income Posting");
|
||||
break;
|
||||
case CREDIT_BALANCE_REFUND:
|
||||
optionData = new LoanTransactionEnumData(LoanTransactionType.CREDIT_BALANCE_REFUND.getValue().longValue(),
|
||||
LoanTransactionType.CREDIT_BALANCE_REFUND.getCode(), "Credit Balance Refund");
|
||||
break;
|
||||
}
|
||||
return optionData;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@
|
||||
<include file="parts/0008_loan_charge_add_external_id.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0009_hold_reason_savings_account.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0010_lien_allowed_on_savings_account_products.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0011_add_credit_balance_refund_permission.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_permission">
|
||||
<column name="grouping" value="transaction_loan" />
|
||||
<column name="code" value="CREDITBALANCEREFUND_LOAN" />
|
||||
<column name="entity_name" value="LOAN" />
|
||||
<column name="action_name" value="CREDITBALANCEREFUND" />
|
||||
<column name="can_maker_checker" valueBoolean="false" />
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.CommonConstants;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.accounting.JournalEntry;
|
||||
import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public class ClientLoanCreditBalanceRefundIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientLoanCreditBalanceRefundIntegrationTest.class);
|
||||
|
||||
private ResponseSpecification responseSpec;
|
||||
private RequestSpecification requestSpec;
|
||||
private LoanTransactionHelper loanTransactionHelper;
|
||||
private LoanTransactionHelper loanTransactionHelperValidationError;
|
||||
private JournalEntryHelper journalEntryHelper;
|
||||
private AccountHelper accountHelper;
|
||||
private Integer disbursedLoanID;
|
||||
private static final String ACCRUAL_PERIODIC = "3";
|
||||
private Account assetAccount;
|
||||
private Account incomeAccount;
|
||||
private Account expenseAccount;
|
||||
private Account overpaymentAccount;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Utils.initializeRESTAssured();
|
||||
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
|
||||
this.loanTransactionHelperValidationError = new LoanTransactionHelper(this.requestSpec, new ResponseSpecBuilder().build());
|
||||
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
|
||||
this.assetAccount = this.accountHelper.createAssetAccount();
|
||||
this.incomeAccount = this.accountHelper.createIncomeAccount();
|
||||
this.expenseAccount = this.accountHelper.createExpenseAccount();
|
||||
this.overpaymentAccount = this.accountHelper.createLiabilityAccount();
|
||||
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
|
||||
|
||||
final String principal = "12000.00";
|
||||
final String submitApproveDisburseDate = "01 January 2022";
|
||||
this.disbursedLoanID = fromStartToDisburseLoan(submitApproveDisburseDate, principal, ACCRUAL_PERIODIC, assetAccount, incomeAccount,
|
||||
expenseAccount, overpaymentAccount);
|
||||
|
||||
}
|
||||
|
||||
private Integer createLoanProduct(final String principal, final boolean multiDisburseLoan, final String accountingRule,
|
||||
final Account... accounts) {
|
||||
LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
|
||||
LoanProductTestBuilder builder = new LoanProductTestBuilder() //
|
||||
.withPrincipal(principal) //
|
||||
.withNumberOfRepayments("4") //
|
||||
.withRepaymentAfterEvery("1") //
|
||||
.withRepaymentTypeAsMonth() //
|
||||
.withinterestRatePerPeriod("1") //
|
||||
.withInterestRateFrequencyTypeAsMonths() //
|
||||
.withAmortizationTypeAsEqualInstallments() //
|
||||
.withInterestTypeAsDecliningBalance() //
|
||||
.withAccounting(accountingRule, accounts) //
|
||||
.withTranches(multiDisburseLoan);
|
||||
if (multiDisburseLoan) {
|
||||
builder = builder.withInterestCalculationPeriodTypeAsRepaymentPeriod(true);
|
||||
builder = builder.withMaxTrancheCount("30");
|
||||
}
|
||||
final String loanProductJSON = builder.build(null);
|
||||
return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
|
||||
}
|
||||
|
||||
private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String principal, String submitDate) {
|
||||
LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
|
||||
final String loanApplicationJSON = new LoanApplicationTestBuilder() //
|
||||
.withPrincipal(principal) //
|
||||
.withLoanTermFrequency("4") //
|
||||
.withLoanTermFrequencyAsMonths() //
|
||||
.withNumberOfRepayments("4") //
|
||||
.withRepaymentEveryAfter("1") //
|
||||
.withRepaymentFrequencyTypeAsMonths() //
|
||||
.withInterestRatePerPeriod("2") //
|
||||
.withAmortizationTypeAsEqualInstallments() //
|
||||
.withInterestTypeAsDecliningBalance() //
|
||||
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
|
||||
.withExpectedDisbursementDate(submitDate) //
|
||||
.withSubmittedOnDate(submitDate) //
|
||||
.build(clientID.toString(), loanProductID.toString(), null);
|
||||
return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
|
||||
}
|
||||
|
||||
private Integer fromStartToDisburseLoan(String submitApproveDisburseDate, String principal, final String accountingRule,
|
||||
final Account... accounts) {
|
||||
|
||||
final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
|
||||
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
|
||||
|
||||
boolean allowMultipleDisbursals = false;
|
||||
final Integer loanProductID = createLoanProduct(principal, allowMultipleDisbursals, accountingRule, accounts);
|
||||
Assertions.assertNotNull(loanProductID);
|
||||
|
||||
final Integer loanID = applyForLoanApplication(clientID, loanProductID, principal, submitApproveDisburseDate);
|
||||
Assertions.assertNotNull(loanID);
|
||||
HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
|
||||
LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
|
||||
|
||||
LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
|
||||
loanStatusHashMap = this.loanTransactionHelper.approveLoan(submitApproveDisburseDate, loanID);
|
||||
LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
|
||||
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
|
||||
|
||||
LOG.info("-------------------------------DISBURSE LOAN -------------------------------------------"); //
|
||||
// String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
|
||||
loanStatusHashMap = this.loanTransactionHelper.disburseLoan(submitApproveDisburseDate, loanID, principal);
|
||||
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
|
||||
return loanID;
|
||||
}
|
||||
|
||||
private HashMap makeRepayment(final String repaymentDate, final Float repayment) {
|
||||
LOG.info("-------------Make repayment -----------");
|
||||
this.loanTransactionHelper.makeRepayment(repaymentDate, repayment, disbursedLoanID);
|
||||
HashMap loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
|
||||
"status");
|
||||
return loanStatusHashMap;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creditBalanceRefundCanOnlyBeAppliedWhereLoanStatusIsOverpaidTest() {
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 2000.00f); // not full payment
|
||||
LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
|
||||
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
final Float refund = 1000.00f;
|
||||
final String externalId = null;
|
||||
ArrayList<HashMap> cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError
|
||||
.creditBalanceRefund(creditBalanceRefundDate, refund, externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
|
||||
|
||||
assertEquals("error.msg.loan.credit.balance.refund.account.is.not.overpaid",
|
||||
cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
|
||||
|
||||
// ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec,
|
||||
// this.responseSpec, loanID);
|
||||
// final int loanScheduleLineCount = loanSchedule.size();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cantRefundMoreThanOverpaidTest() {
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
Float refund = 10000.00f;
|
||||
final String externalId = null;
|
||||
ArrayList<HashMap> cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError
|
||||
.creditBalanceRefund(creditBalanceRefundDate, refund, externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
|
||||
|
||||
assertEquals("error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount",
|
||||
cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
|
||||
|
||||
refund = (float) -1.00;
|
||||
cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError.creditBalanceRefund(creditBalanceRefundDate, refund,
|
||||
externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
|
||||
assertEquals("validation.msg.loan.transaction.transactionAmount.not.greater.than.zero",
|
||||
cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fullRefundChangesStatusToClosedObligationMetTest() {
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
final Float totalOverpaid = (Float) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
|
||||
"totalOverpaid");
|
||||
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
final String externalId = null;
|
||||
loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, totalOverpaid, externalId, disbursedLoanID, null);
|
||||
loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
|
||||
"status");
|
||||
LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap);
|
||||
|
||||
final Float floatZero = 0.0f;
|
||||
Float totalOverpaidAtEnd = (Float) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
|
||||
"totalOverpaid");
|
||||
if (totalOverpaidAtEnd == null) {
|
||||
totalOverpaidAtEnd = floatZero;
|
||||
}
|
||||
assertEquals(totalOverpaidAtEnd, floatZero);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialRefundKeepsOverpaidStatusTest() {
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
final Float refund = 5000.00f; // partial refund
|
||||
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
final String externalId = null;
|
||||
loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId, disbursedLoanID, null);
|
||||
loanStatusHashMap = (HashMap) this.loanTransactionHelper.getLoanDetail(this.requestSpec, this.responseSpec, disbursedLoanID,
|
||||
"status");
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newCreditBalanceRefundSavesExternalIdTest() {
|
||||
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
final Float refund = 1000.00f; // partial refund
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
final String externalId = "cbrextID" + disbursedLoanID.toString();
|
||||
Integer resourceId = (Integer) loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId,
|
||||
disbursedLoanID, "resourceId");
|
||||
Assertions.assertNotNull(resourceId);
|
||||
|
||||
HashMap creditBalanceRefundMap = this.loanTransactionHelper.getLoanTransactionDetails(disbursedLoanID, resourceId);
|
||||
Assertions.assertNotNull(creditBalanceRefundMap.get("externalId"));
|
||||
Assertions.assertEquals(creditBalanceRefundMap.get("externalId"), externalId, "Incorrect External Id Saved");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newCreditBalanceRefundFindsDuplicateExternalIdTest() {
|
||||
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
final Float refund = 1000.00f; // partial refund
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
final String externalId = "cbrextID" + disbursedLoanID.toString();
|
||||
final Integer resourceId = (Integer) loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId,
|
||||
disbursedLoanID, "resourceId");
|
||||
Assertions.assertNotNull(resourceId);
|
||||
|
||||
final Float refund2 = 10.00f; // partial refund
|
||||
final String creditBalanceRefundDate2 = "10 January 2022";
|
||||
ArrayList<HashMap> cbrErrors = (ArrayList<HashMap>) loanTransactionHelperValidationError
|
||||
.creditBalanceRefund(creditBalanceRefundDate2, refund2, externalId, disbursedLoanID, CommonConstants.RESPONSE_ERROR);
|
||||
assertEquals("error.msg.loan.creditBalanceRefund.duplicate.externalId",
|
||||
cbrErrors.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newCreditBalanceRefundCreatesCorrectJournalEntriesForPeriodicAccrualsTest() {
|
||||
|
||||
HashMap loanStatusHashMap = makeRepayment("06 January 2022", 20000.00f); // overpayment
|
||||
LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap);
|
||||
|
||||
final Float refund = 1000.00f; // partial refund
|
||||
final String creditBalanceRefundDate = "09 January 2022";
|
||||
final String externalId = null;
|
||||
final Integer resourceId = (Integer) loanTransactionHelper.creditBalanceRefund(creditBalanceRefundDate, refund, externalId,
|
||||
disbursedLoanID, "resourceId");
|
||||
Assertions.assertNotNull(resourceId);
|
||||
|
||||
this.journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, creditBalanceRefundDate,
|
||||
new JournalEntry(refund, JournalEntry.TransactionType.DEBIT));
|
||||
this.journalEntryHelper.checkJournalEntryForLiabilityAccount(overpaymentAccount, creditBalanceRefundDate,
|
||||
new JournalEntry(refund, JournalEntry.TransactionType.CREDIT));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -46,9 +46,9 @@ import org.slf4j.LoggerFactory;
|
||||
* repayments and verifying accounting transactions
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public class ClientLoanIntegrationMultipleDisbursementsTest {
|
||||
public class ClientLoanMultipleDisbursementsIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientLoanIntegrationMultipleDisbursementsTest.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClientLoanMultipleDisbursementsIntegrationTest.class);
|
||||
|
||||
public static final String MINIMUM_OPENING_BALANCE = "1000.0";
|
||||
public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
|
||||
+26
@@ -63,6 +63,7 @@ public class LoanTransactionHelper {
|
||||
private static final String WRITE_OFF_LOAN_COMMAND = "writeoff";
|
||||
private static final String WAIVE_INTEREST_COMMAND = "waiveinterest";
|
||||
private static final String MAKE_REPAYMENT_COMMAND = "repayment";
|
||||
private static final String CREDIT_BALANCE_REFUND_COMMAND = "creditBalanceRefund";
|
||||
private static final String WITHDRAW_LOAN_APPLICATION_COMMAND = "withdrawnByApplicant";
|
||||
private static final String RECOVER_FROM_GUARANTORS_COMMAND = "recoverGuarantees";
|
||||
private static final String MAKE_REFUND_BY_CASH_COMMAND = "refundByCash";
|
||||
@@ -332,6 +333,12 @@ public class LoanTransactionHelper {
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
public Object creditBalanceRefund(final String date, final Float amountToBePaid, final String externalId, final Integer loanID,
|
||||
String jsonAttributeToGetback) {
|
||||
return performLoanTransaction(createLoanTransactionURL(CREDIT_BALANCE_REFUND_COMMAND, loanID),
|
||||
getCreditBalanceRefundBodyAsJSON(date, amountToBePaid, externalId), jsonAttributeToGetback);
|
||||
}
|
||||
|
||||
public HashMap makeRepayment(final String date, final Float amountToBePaid, final Integer loanID) {
|
||||
return (HashMap) performLoanTransaction(createLoanTransactionURL(MAKE_REPAYMENT_COMMAND, loanID),
|
||||
getRepaymentBodyAsJSON(date, amountToBePaid), "");
|
||||
@@ -425,6 +432,12 @@ public class LoanTransactionHelper {
|
||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, "");
|
||||
}
|
||||
|
||||
public HashMap getLoanTransactionDetails(final Integer loanId, final Integer txnId) {
|
||||
final String GET_LOAN_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/" + txnId + "?"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_CHARGES_URL, "");
|
||||
}
|
||||
|
||||
public HashMap getPostDatedCheck(final Integer loanId, final Integer installmentId) {
|
||||
final String GET_POST_DATED_TRANS_URL = "/fineract-provider/api/v1/loans/" + loanId + "/postdatedchecks/" + installmentId + "?"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
@@ -500,6 +513,19 @@ public class LoanTransactionHelper {
|
||||
return new Gson().toJson(map);
|
||||
}
|
||||
|
||||
private String getCreditBalanceRefundBodyAsJSON(final String transactionDate, final Float transactionAmount, final String externalId) {
|
||||
final HashMap<String, String> map = new HashMap<>();
|
||||
map.put("locale", "en");
|
||||
map.put("dateFormat", "dd MMMM yyyy");
|
||||
map.put("transactionDate", transactionDate);
|
||||
map.put("transactionAmount", transactionAmount.toString());
|
||||
map.put("note", "Credit Balance Refund Made!!!");
|
||||
if (externalId != null) {
|
||||
map.put("externalId", externalId);
|
||||
}
|
||||
return new Gson().toJson(map);
|
||||
}
|
||||
|
||||
private String getRepaymentBodyAsJSON(final String transactionDate, final Float transactionAmount) {
|
||||
final HashMap<String, String> map = new HashMap<>();
|
||||
map.put("locale", "en");
|
||||
|
||||
Reference in New Issue
Block a user