diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index c95fd3710..9a0fbcc60 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -651,6 +651,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return getTypeOf().isChargeback() && isNotReversed(); } + public boolean isEditable() { + return !(isChargeback() || isGoodwillCredit() || isPayoutRefund() || isMerchantIssuedRefund()); + } + public boolean isPenaltyPayment() { boolean isPenalty = false; if (isChargePayment()) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentAdjustmentCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentAdjustmentCommandHandler.java index e04e9bf6f..3e313f152 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentAdjustmentCommandHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanRepaymentAdjustmentCommandHandler.java @@ -37,7 +37,6 @@ public class LoanRepaymentAdjustmentCommandHandler implements NewCommandSourceHa @Transactional @Override public CommandProcessingResult processCommand(final JsonCommand command) { - return this.writePlatformService.adjustLoanTransaction(command.getLoanId(), command.entityId(), command); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 31139afe4..5b0aca276 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -163,6 +163,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepositor import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException; @@ -1066,16 +1067,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf transactionId); } - if (transactionToAdjust.isChargeback()) { - throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", "Loan transaction:" - + transactionId + " update not allowed as loan transaction is charge back and is linked to other transaction", - transactionId); - } - final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); + final boolean isAdjustCommand = (transactionAmount.compareTo(BigDecimal.ZERO) > 0); + if (isAdjustCommand && !transactionToAdjust.isEditable()) { + final String errorMessage = "Loan transaction: " + transactionId + " update not allowed as loan transaction is a " + + transactionToAdjust.getTypeOf().getCode(); + throw new InvalidLoanTransactionTypeException("transaction", "error.msg.loan.transaction.update.not.allowed", errorMessage); + } + // We dont need auto generation for reversal external id... if it is not provided, it remains null (empty) final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java index 45091e9f4..1feb1c1c7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.java @@ -27,6 +27,7 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.Utils; @@ -50,6 +51,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest.class); private ResponseSpecification responseSpec; + private ResponseSpecification responseSpec403; private RequestSpecification requestSpec; private LoanTransactionHelper loanTransactionHelper; private LoanTransactionHelper loanTransactionHelperValidationError; @@ -73,6 +75,7 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.responseSpec403 = new ResponseSpecBuilder().expectStatusCode(403).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); @@ -438,4 +441,142 @@ public class ClientLoanCreditBalanceRefundandRepaymentTypeIntegrationTest { } + @Test + public void undoGoodWillCreditTransactionTest() { + // Given + disburseLoanOfAccountingRule(CASH_BASED); + HashMap loanSummaryMap = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, disbursedLoanID); + + // pay off all of principal, interest (no fees or penalties) + final Float principalOutstanding = (Float) loanSummaryMap.get("principalOutstanding"); + final Float interestOutstanding = (Float) loanSummaryMap.get("interestOutstanding"); + final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); + final Float overpaidAmount = 159.00f; + final Float transactionAmount = totalOutstanding + overpaidAmount; + final String transactionDate = "09 January 2022"; + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(GOODWILL_CREDIT, + transactionDate, transactionAmount, this.disbursedLoanID); + Assertions.assertNotNull(loanTransactionResponse); + Assertions.assertNotNull(loanTransactionResponse.getResourceId()); + + // Then + loanTransactionHelper.reverseLoanTransaction(this.disbursedLoanID, loanTransactionResponse.getResourceId(), transactionDate, + responseSpec); + } + + @Test + public void undoPayoutRefundTransactionTest() { + // Given + disburseLoanOfAccountingRule(CASH_BASED); + HashMap loanSummaryMap = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, disbursedLoanID); + + // pay off all of principal, interest (no fees or penalties) + final Float principalOutstanding = (Float) loanSummaryMap.get("principalOutstanding"); + final Float interestOutstanding = (Float) loanSummaryMap.get("interestOutstanding"); + final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); + final Float overpaidAmount = 159.00f; + final Float transactionAmount = totalOutstanding + overpaidAmount; + final String transactionDate = "09 January 2022"; + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(PAYOUT_REFUND, + transactionDate, transactionAmount, this.disbursedLoanID); + Assertions.assertNotNull(loanTransactionResponse); + Assertions.assertNotNull(loanTransactionResponse.getResourceId()); + + // Then + loanTransactionHelper.reverseLoanTransaction(this.disbursedLoanID, loanTransactionResponse.getResourceId(), transactionDate, + responseSpec); + } + + @Test + public void undoMerchantIssuedRefundTransactionTest() { + // Given + disburseLoanOfAccountingRule(CASH_BASED); + HashMap loanSummaryMap = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, disbursedLoanID); + + // pay off all of principal, interest (no fees or penalties) + final Float principalOutstanding = (Float) loanSummaryMap.get("principalOutstanding"); + final Float interestOutstanding = (Float) loanSummaryMap.get("interestOutstanding"); + final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); + final Float overpaidAmount = 159.00f; + final Float transactionAmount = totalOutstanding + overpaidAmount; + final String transactionDate = "09 January 2022"; + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(MERCHANT_ISSUED_REFUND, + transactionDate, transactionAmount, this.disbursedLoanID); + Assertions.assertNotNull(loanTransactionResponse); + Assertions.assertNotNull(loanTransactionResponse.getResourceId()); + + // Then + loanTransactionHelper.reverseLoanTransaction(this.disbursedLoanID, loanTransactionResponse.getResourceId(), transactionDate, + responseSpec); + } + + @Test + public void adjustGoodWillCreditTransactionTest() { + // Given + disburseLoanOfAccountingRule(CASH_BASED); + HashMap loanSummaryMap = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, disbursedLoanID); + + // pay off all of principal, interest (no fees or penalties) + final Float principalOutstanding = (Float) loanSummaryMap.get("principalOutstanding"); + final Float interestOutstanding = (Float) loanSummaryMap.get("interestOutstanding"); + final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); + final Float overpaidAmount = 159.00f; + final Float transactionAmount = totalOutstanding + overpaidAmount; + final String transactionDate = "09 January 2022"; + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(GOODWILL_CREDIT, + transactionDate, transactionAmount, this.disbursedLoanID); + Assertions.assertNotNull(loanTransactionResponse); + Assertions.assertNotNull(loanTransactionResponse.getResourceId()); + + // Then + loanTransactionHelper.adjustLoanTransaction(this.disbursedLoanID, loanTransactionResponse.getResourceId(), transactionDate, + responseSpec403); + } + + @Test + public void adjustPayoutRefundTransactionTest() { + // Given + disburseLoanOfAccountingRule(CASH_BASED); + HashMap loanSummaryMap = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, disbursedLoanID); + + // pay off all of principal, interest (no fees or penalties) + final Float principalOutstanding = (Float) loanSummaryMap.get("principalOutstanding"); + final Float interestOutstanding = (Float) loanSummaryMap.get("interestOutstanding"); + final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); + final Float overpaidAmount = 159.00f; + final Float transactionAmount = totalOutstanding + overpaidAmount; + final String transactionDate = "09 January 2022"; + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(PAYOUT_REFUND, + transactionDate, transactionAmount, this.disbursedLoanID); + Assertions.assertNotNull(loanTransactionResponse); + Assertions.assertNotNull(loanTransactionResponse.getResourceId()); + + // Then + loanTransactionHelper.adjustLoanTransaction(this.disbursedLoanID, loanTransactionResponse.getResourceId(), transactionDate, + responseSpec403); + } + + @Test + public void adjustMerchantIssuedRefundTransactionTest() { + // Given + disburseLoanOfAccountingRule(CASH_BASED); + HashMap loanSummaryMap = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, disbursedLoanID); + + // pay off all of principal, interest (no fees or penalties) + final Float principalOutstanding = (Float) loanSummaryMap.get("principalOutstanding"); + final Float interestOutstanding = (Float) loanSummaryMap.get("interestOutstanding"); + final Float totalOutstanding = (Float) loanSummaryMap.get("totalOutstanding"); + final Float overpaidAmount = 159.00f; + final Float transactionAmount = totalOutstanding + overpaidAmount; + final String transactionDate = "09 January 2022"; + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(MERCHANT_ISSUED_REFUND, + transactionDate, transactionAmount, this.disbursedLoanID); + Assertions.assertNotNull(loanTransactionResponse); + Assertions.assertNotNull(loanTransactionResponse.getResourceId()); + + // Then + loanTransactionHelper.adjustLoanTransaction(this.disbursedLoanID, loanTransactionResponse.getResourceId(), transactionDate, + responseSpec403); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java index 7a15c9453..b5bbdfa6d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionChargebackTest.java @@ -57,6 +57,7 @@ public class LoanTransactionChargebackTest { private ResponseSpecification responseSpec; private ResponseSpecification responseSpecErr400; + private ResponseSpecification responseSpecErr403; private ResponseSpecification responseSpecErr503; private RequestSpecification requestSpec; private LoanTransactionHelper loanTransactionHelper; @@ -71,6 +72,7 @@ public class LoanTransactionChargebackTest { this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); this.responseSpecErr400 = new ResponseSpecBuilder().expectStatusCode(400).build(); + this.responseSpecErr403 = new ResponseSpecBuilder().expectStatusCode(403).build(); this.responseSpecErr503 = new ResponseSpecBuilder().expectStatusCode(503).build(); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); @@ -114,12 +116,30 @@ public class LoanTransactionChargebackTest { // Try to reverse a Loan Transaction charge back PostLoansLoanIdTransactionsResponse reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId, - chargebackTransactionId, operationDate, responseSpecErr503); + chargebackTransactionId, operationDate, responseSpecErr403); // Try to reverse a Loan Transaction repayment with linked transactions reverseTransactionResponse = loanTransactionHelper.reverseLoanTransaction(loanId, transactionId, operationDate, responseSpecErr503); } + @Test + public void applyAndAdjustLoanTransactionChargeback() { + // Client and Loan account creation + final Integer loanId = createAccounts(15, 1); + + Float amount = Float.valueOf(amountVal); + PostLoansLoanIdTransactionsResponse loanTransactionResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, + loanId); + assertNotNull(loanTransactionResponse); + final Long transactionId = loanTransactionResponse.getResourceId(); + + final Long chargebackTransactionId = loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "1000.00", 0, + responseSpec); + + // Then + loanTransactionHelper.adjustLoanTransaction(loanId, chargebackTransactionId, operationDate, responseSpecErr403); + } + @Test public void applyLoanTransactionChargebackWithAmountZero() { // Client and Loan account creation @@ -141,7 +161,6 @@ public class LoanTransactionChargebackTest { loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); loanTransactionHelper.applyChargebackTransaction(loanId, transactionId, "0.00", 0, responseSpecErr400); - } @Test diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index c2ee76342..cb3ba3993 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -504,7 +504,14 @@ public class LoanTransactionHelper extends IntegrationTest { } public HashMap reverseRepayment(final Integer loanId, final Integer transactionId, String date) { - return (HashMap) performLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), getUndoJsonBody(date), ""); + return (HashMap) performLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId), + getAdjustTransactionJsonBody(date, "0"), ""); + } + + public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String repaymentTypeCommand, final String date, + final Float amountToBePaid, final Integer loanID) { + log.info("Repayment with amount {} in {} for Loan {}", amountToBePaid, date, loanID); + return postLoanTransaction(createLoanTransactionURL(repaymentTypeCommand, loanID), getRepaymentBodyAsJSON(date, amountToBePaid)); } public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final String date, final Float amountToBePaid, final Integer loanID) { @@ -701,9 +708,16 @@ public class LoanTransactionHelper extends IntegrationTest { return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId, request, "adjust")); } + public PostLoansLoanIdTransactionsResponse adjustLoanTransaction(final Integer loanId, final Long transactionId, String date, + ResponseSpecification responseSpec) { + return postLoanTransaction(createLoanTransactionURL(null, loanId, transactionId.intValue()), + getAdjustTransactionJsonBody(date, "10"), responseSpec); + } + public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Integer loanId, final Long transactionId, String date, ResponseSpecification responseSpec) { - return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId.intValue()), getUndoJsonBody(date), responseSpec); + return postLoanTransaction(createLoanTransactionURL(UNDO, loanId, transactionId.intValue()), + getAdjustTransactionJsonBody(date, "0"), responseSpec); } public PostLoansLoanIdTransactionsResponse reverseLoanTransaction(final Long loanId, final Long transactionId, @@ -1074,10 +1088,10 @@ public class LoanTransactionHelper extends IntegrationTest { return new Gson().toJson(map); } - private String getUndoJsonBody(String date) { + private String getAdjustTransactionJsonBody(String date, String amount) { final HashMap map = new HashMap<>(); map.put("transactionDate", date); - map.put("transactionAmount", "0"); + map.put("transactionAmount", amount); map.put("dateFormat", "dd MMMM yyyy"); map.put("locale", "en"); return new Gson().toJson(map); @@ -1257,8 +1271,11 @@ public class LoanTransactionHelper extends IntegrationTest { } private String createLoanTransactionURL(final String command, final Integer loanID, final Integer transactionId) { - return "/fineract-provider/api/v1/loans/" + loanID + "/transactions/" + transactionId + "?command=" + command + "&" - + Utils.TENANT_IDENTIFIER; + String url = "/fineract-provider/api/v1/loans/" + loanID + "/transactions/" + transactionId + "?"; + if (command != null) { + url = url + "command=" + command + "&"; + } + return url + Utils.TENANT_IDENTIFIER; } private String createGlimAccountURL(final String command, final Integer glimID) {