FINERACT-1761: New Charge Adjustment transaction

This commit is contained in:
Adam Saghy
2022-11-10 09:36:10 +01:00
parent 48bbc9b266
commit cb1d1d0d5d
79 changed files with 3391 additions and 1613 deletions
@@ -120,16 +120,16 @@ final class GLAccountsApiResourceSwagger {
public Boolean manualEntriesAllowed;
@Schema(example = "1")
public String type;
public Integer type;
@Schema(example = "10")
public String tagId;
public Long tagId;
@Schema(example = "1")
public Long parentId;
@Schema(example = "1")
public String usage;
public Integer usage;
@Schema(example = "Desc")
public String description;
@@ -144,7 +144,7 @@ final class GLAccountsApiResourceSwagger {
}
@Schema(example = "22")
public int resourceId;
public Long resourceId;
}
@Schema(description = "PutGLAccountsRequest")
@@ -175,7 +175,7 @@ final class GLAccountsApiResourceSwagger {
}
@Schema(example = "1")
public int resourceId;
public Long resourceId;
public PutGLAccountsResponsechangesSwagger changes;
}
@@ -193,7 +193,7 @@ final class GLAccountsApiResourceSwagger {
}
@Schema(example = "1")
public int resourceId;
public Long resourceId;
private DeleteGLAccountsRequestchangesSwagger changes;
}
@@ -24,6 +24,7 @@ import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
@RequiredArgsConstructor
@@ -58,4 +59,5 @@ public class LoanTransactionDTO {
private boolean isLoanToLoanTransfer;
private final String chargeRefundChargeType;
private final LoanChargeData loanChargeData;
}
@@ -66,6 +66,7 @@ import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatfor
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientTransaction;
import org.apache.fineract.portfolio.client.domain.ClientTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
@@ -125,6 +126,7 @@ public class AccountingProcessorHelper {
final boolean reversed = (Boolean) map.get("reversed");
final Long paymentTypeId = (Long) map.get("paymentTypeId");
final String chargeRefundChargeType = (String) map.get("chargeRefundChargeType");
final LoanChargeData loanChargeData = (LoanChargeData) map.get("loanChargeData");
final List<ChargePaymentDTO> feePaymentDetails = new ArrayList<>();
final List<ChargePaymentDTO> penaltyPaymentDetails = new ArrayList<>();
@@ -152,7 +154,7 @@ public class AccountingProcessorHelper {
}
final LoanTransactionDTO transaction = new LoanTransactionDTO(transactionOfficeId, paymentTypeId, transactionId,
transactionDate, transactionType, amount, principal, interest, fees, penalties, overPayments, reversed,
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType);
penaltyPaymentDetails, feePaymentDetails, isAccountTransfer, chargeRefundChargeType, loanChargeData);
Boolean isLoanToLoanTransfer = (Boolean) accountingBridgeData.get("isLoanToLoanTransfer");
transaction.setLoanToLoanTransfer(isLoanToLoanTransfer != null && isLoanToLoanTransfer);
newLoanTransactions.add(transaction);
@@ -622,6 +624,17 @@ public class AccountingProcessorHelper {
}
}
public void createDebitJournalEntryOrReversalForLoanCharges(final Office office, final String currencyCode,
final int accountMappingTypeId, final Long loanProductId, final Long chargeId, final Long loanId, final String transactionId,
final LocalDate transactionDate, final BigDecimal amount, final Boolean isReversal) {
final GLAccount account = getLinkedGLAccountForLoanCharges(loanProductId, accountMappingTypeId, chargeId);
if (isReversal) {
createCreditJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount);
} else {
createDebitJournalEntryForLoan(office, currencyCode, account, loanId, transactionId, transactionDate, amount);
}
}
public void createCreditJournalEntryOrReversalForLoanCharges(final Office office, final String currencyCode,
final int accountMappingTypeId, final Long loanProductId, final Long loanId, final String transactionId,
final LocalDate transactionDate, final BigDecimal totalAmount, final Boolean isReversal,
@@ -61,9 +61,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
/***
* Handle repayments, loan refunds, repayments at disbursement and reversal of Repayments and Repayments at
* disbursement
* disbursement (except charge adjustment)
***/
else if (loanTransactionDTO.getTransactionType().isRepaymentType()
else if ((loanTransactionDTO.getTransactionType().isRepaymentType()
&& !loanTransactionDTO.getTransactionType().isChargeAdjustment())
|| loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()
|| loanTransactionDTO.getTransactionType().isChargePayment()) {
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false,
@@ -99,6 +100,109 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
else if (loanTransactionDTO.getTransactionType().isChargeback()) {
createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office);
}
// Logic for Charge Adjustment
else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) {
createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office);
}
}
}
private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, 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 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 overPaymentAmount = loanTransactionDTO.getOverPayment();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final boolean isReversal = loanTransactionDTO.isReversed();
BigDecimal totalDebitAmount = new BigDecimal(0);
Map<GLAccount, BigDecimal> accountMap = new HashMap<>();
// handle principal payment (and reversals)
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
accountMap.put(account, principalAmount);
}
// handle interest payment (and reversals)
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(interestAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, interestAmount);
}
}
// handle fees payment (and reversals)
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(feesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, feesAmount);
}
}
// handle penalties payment (and reversals)
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, penaltiesAmount);
}
}
// handle overpayment
if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(overPaymentAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, overPaymentAmount);
}
}
for (Map.Entry<GLAccount, BigDecimal> entry : accountMap.entrySet()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate,
entry.getValue(), isReversal, entry.getKey());
}
if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId();
Integer accountMappingTypeId;
if (loanTransactionDTO.getLoanChargeData().isPenalty()) {
accountMappingTypeId = AccrualAccountsForLoan.INCOME_FROM_PENALTIES.getValue();
} else {
accountMappingTypeId = AccrualAccountsForLoan.INCOME_FROM_FEES.getValue();
}
this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId,
loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
}
}
@@ -21,11 +21,15 @@ package org.apache.fineract.accounting.journalentry.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForLoan;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
@@ -58,9 +62,10 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
}
/***
* Logic for repayments, repayments at disbursement and reversal of Repayments and Repayments at
* disbursement
* disbursement (except charge adjustment)
***/
else if (loanTransactionDTO.getTransactionType().isRepaymentType()
else if ((loanTransactionDTO.getTransactionType().isRepaymentType()
&& !loanTransactionDTO.getTransactionType().isChargeAdjustment())
|| loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()
|| loanTransactionDTO.getTransactionType().isChargePayment()) {
createJournalEntriesForRepayments(loanDTO, loanTransactionDTO, office);
@@ -106,6 +111,109 @@ public class CashBasedAccountingProcessorForLoan implements AccountingProcessorF
else if (loanTransactionDTO.getTransactionType().isChargeback()) {
createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office);
}
// Logic for Charge Adjustment
else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) {
createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office);
}
}
}
private void createJournalEntriesForChargeAdjustment(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO, 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 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 overPaymentAmount = loanTransactionDTO.getOverPayment();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final boolean isReversal = loanTransactionDTO.isReversed();
BigDecimal totalDebitAmount = new BigDecimal(0);
Map<GLAccount, BigDecimal> accountMap = new HashMap<>();
// handle principal payment (and reversals)
if (principalAmount != null && !(principalAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(principalAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.CashAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
accountMap.put(account, principalAmount);
}
// handle interest payment (and reversals)
if (interestAmount != null && !(interestAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(interestAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.CashAccountsForLoan.INTEREST_ON_LOANS.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(interestAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, interestAmount);
}
}
// handle fees payment (and reversals)
if (feesAmount != null && !(feesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(feesAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.CashAccountsForLoan.INCOME_FROM_FEES.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(feesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, feesAmount);
}
}
// handle penalties payment (and reversals)
if (penaltiesAmount != null && !(penaltiesAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(penaltiesAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(penaltiesAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, penaltiesAmount);
}
}
// handle overpayment
if (overPaymentAmount != null && !(overPaymentAmount.compareTo(BigDecimal.ZERO) == 0)) {
totalDebitAmount = totalDebitAmount.add(overPaymentAmount);
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccountingConstants.CashAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId);
if (accountMap.containsKey(account)) {
BigDecimal amount = accountMap.get(account).add(overPaymentAmount);
accountMap.put(account, amount);
} else {
accountMap.put(account, overPaymentAmount);
}
}
for (Map.Entry<GLAccount, BigDecimal> entry : accountMap.entrySet()) {
this.helper.createCreditJournalEntryOrReversalForLoan(office, currencyCode, loanId, transactionId, transactionDate,
entry.getValue(), isReversal, entry.getKey());
}
if (!(totalDebitAmount.compareTo(BigDecimal.ZERO) == 0)) {
Long chargeId = loanTransactionDTO.getLoanChargeData().getChargeId();
Integer accountMappingTypeId;
if (loanTransactionDTO.getLoanChargeData().isPenalty()) {
accountMappingTypeId = AccountingConstants.CashAccountsForLoan.INCOME_FROM_PENALTIES.getValue();
} else {
accountMappingTypeId = AccountingConstants.CashAccountsForLoan.INCOME_FROM_FEES.getValue();
}
this.helper.createDebitJournalEntryOrReversalForLoanCharges(office, currencyCode, accountMappingTypeId, loanProductId, chargeId,
loanId, transactionId, transactionDate, totalDebitAmount, isReversal);
}
}
@@ -22,8 +22,8 @@ import java.util.Collection;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.stereotype.Component;
@Component
@@ -31,14 +31,14 @@ import org.springframework.stereotype.Component;
public class ApplyChargeToOverdueLoansBusinessStep implements LoanCOBBusinessStep {
private final LoanReadPlatformService loanReadPlatformService;
private final LoanWritePlatformService loanWritePlatformService;
private final LoanChargeWritePlatformService loanChargeWritePlatformService;
@Override
public Loan execute(Loan loan) {
final Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList = loanReadPlatformService
.retrieveAllOverdueInstallmentsForLoan(loan);
loanWritePlatformService.applyOverdueChargesForLoan(loan.getId(), overdueLoanScheduleDataList);
loanChargeWritePlatformService.applyOverdueChargesForLoan(loan.getId(), overdueLoanScheduleDataList);
return loan;
}
@@ -787,6 +787,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder adjustmentForLoanCharge(final Long loanId, final Long loanChargeId) {
this.actionName = "ADJUSTMENT";
this.entityName = "LOANCHARGE";
this.entityId = loanChargeId;
this.loanId = loanId;
this.href = "/loans/" + loanId + "/charges/" + loanChargeId;
return this;
}
public CommandWrapperBuilder deleteLoanCharge(final Long loanId, final Long loanChargeId) {
this.actionName = "DELETE";
this.entityName = "LOANCHARGE";
@@ -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 LoanChargeAdjustmentPostBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanChargeAdjustmentPostBusinessEvent";
public LoanChargeAdjustmentPostBusinessEvent(LoanTransaction value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -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 LoanChargeAdjustmentPreBusinessEvent extends LoanBusinessEvent {
private static final String TYPE = "LoanChargeAdjustmentPreBusinessEvent";
public LoanChargeAdjustmentPreBusinessEvent(Loan value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -74,7 +74,10 @@ public class CurrencyData implements Serializable {
private String generateDisplayLabel() {
final StringBuilder builder = new StringBuilder(this.name).append(' ');
final StringBuilder builder = new StringBuilder(20);
if (this.name != null) {
builder.append(this.name).append(' ');
}
if (this.displaySymbol != null && !"".equalsIgnoreCase(this.displaySymbol.trim())) {
builder.append('(').append(this.displaySymbol).append(')');
@@ -52,7 +52,7 @@ final class ChargesApiResourceSwagger {
private GetChargesTimeTypeResponse() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargeTimeType.disbursement")
public String code;
@Schema(example = "Disbursement")
@@ -64,7 +64,7 @@ final class ChargesApiResourceSwagger {
private GetChargesAppliesToResponse() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargeAppliesTo.loan")
public String code;
@Schema(example = "Loan")
@@ -76,7 +76,7 @@ final class ChargesApiResourceSwagger {
private GetChargesCalculationTypeResponse() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargeCalculationType.flat")
public String code;
@Schema(example = "Flat")
@@ -88,7 +88,7 @@ final class ChargesApiResourceSwagger {
private GetChargesPaymentModeResponse() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargepaymentmode.accounttransfer")
public String code;
@Schema(example = "Account Transfer")
@@ -100,12 +100,12 @@ final class ChargesApiResourceSwagger {
@Schema(example = "Loan Service fee")
public String name;
@Schema(example = "true")
public String active;
public boolean active;
@Schema(example = "false")
public String penalty;
public boolean penalty;
public GetChargesCurrencyResponse currency;
@Schema(example = "230.56")
public Float amount;
public Double amount;
public GetChargesTimeTypeResponse chargeTimeType;
public GetChargesAppliesToResponse chargeAppliesTo;
public GetChargesCalculationTypeResponse chargeCalculationType;
@@ -126,7 +126,7 @@ final class ChargesApiResourceSwagger {
@Schema(example = "en")
public String locale;
@Schema(example = "230.56")
public Float amount;
public Double amount;
@Schema(example = "1")
public Integer chargeTimeType;
@Schema(example = "1")
@@ -134,11 +134,11 @@ final class ChargesApiResourceSwagger {
@Schema(example = "1")
public Integer chargePaymentMode;
@Schema(example = "true")
public String active;
public boolean active;
@Schema(example = "dd MMMM")
public String monthDayFormat;
@Schema(example = "false")
public String penalty;
public boolean penalty;
}
@Schema(description = "PostChargesResponse")
@@ -147,7 +147,7 @@ final class ChargesApiResourceSwagger {
private PostChargesResponse() {}
@Schema(example = "1")
public Integer resourceId;
public Long resourceId;
}
@Schema(description = "PutChargesChargeIdRequest")
@@ -165,7 +165,7 @@ final class ChargesApiResourceSwagger {
private PutChargesChargeIdResponse() {}
@Schema(example = "1")
public Integer resourceId;
public Long resourceId;
public PutChargesChargeIdRequest changes;
}
@@ -175,7 +175,7 @@ final class ChargesApiResourceSwagger {
private DeleteChargesChargeIdResponse() {}
@Schema(example = "1")
public Integer resourceId;
public Long resourceId;
}
@Schema(description = "GetChargesTemplateResponse")
@@ -188,7 +188,7 @@ final class ChargesApiResourceSwagger {
private GetChargesTemplateLoanChargeCalculationTypeOptions() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargeCalculationType.flat")
public String code;
@Schema(example = "Flat")
@@ -200,7 +200,7 @@ final class ChargesApiResourceSwagger {
private GetChargesTemplateLoanChargeTimeTypeOptions() {}
@Schema(example = "2")
public Integer id;
public Long id;
@Schema(example = "chargeTimeType.specifiedDueDate")
public String code;
@Schema(example = "Specified due date")
@@ -212,7 +212,7 @@ final class ChargesApiResourceSwagger {
private GetChargesTemplateFeeFrequencyOptions() {}
@Schema(example = "0")
public Integer id;
public Long id;
@Schema(example = "loanTermFrequency.periodFrequencyType.days")
public String code;
@Schema(example = "Days")
@@ -220,9 +220,9 @@ final class ChargesApiResourceSwagger {
}
@Schema(example = "false")
public String active;
public boolean active;
@Schema(example = "false")
public String penalty;
public boolean penalty;
public Set<GetChargesResponse.GetChargesCurrencyResponse> currencyOptions;
public Set<GetChargesResponse.GetChargesCalculationTypeResponse> chargeCalculationTypeOptions;
public Set<GetChargesResponse.GetChargesAppliesToResponse> chargeAppliesToOptions;
@@ -657,8 +657,11 @@ public class Charge extends AbstractPersistableCustom {
final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(this.chargeTimeType);
final EnumOptionData chargeAppliesTo = ChargeEnumerations.chargeAppliesTo(this.chargeAppliesTo);
final EnumOptionData chargeCalculationType = ChargeEnumerations.chargeCalculationType(this.chargeCalculation);
final EnumOptionData chargePaymentmode = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode);
final EnumOptionData feeFrequencyType = ChargeEnumerations.chargePaymentMode(this.feeFrequency);
final EnumOptionData chargePaymentMode = ChargeEnumerations.chargePaymentMode(this.chargePaymentMode);
EnumOptionData feeFrequencyType = null;
if (this.feeFrequency != null) {
feeFrequencyType = ChargeEnumerations.feeFrequencyType(this.feeFrequency);
}
GLAccountData accountData = null;
if (account != null) {
accountData = new GLAccountData().setId(account.getId()).setName(account.getName()).setGlCode(account.getGlCode());
@@ -675,7 +678,7 @@ public class Charge extends AbstractPersistableCustom {
final CurrencyData currency = new CurrencyData(this.currencyCode, null, 0, 0, null, null);
return ChargeData.instance(getId(), this.name, this.amount, currency, chargeTimeType, chargeAppliesTo, chargeCalculationType,
chargePaymentmode, getFeeOnMonthDay(), this.feeInterval, this.penalty, this.active, this.enableFreeWithdrawal,
chargePaymentMode, getFeeOnMonthDay(), this.feeInterval, this.penalty, this.active, this.enableFreeWithdrawal,
this.freeWithdrawalFrequency, this.restartFrequency, this.restartFrequencyEnum, this.enablePaymentType, paymentTypeData,
this.minCap, this.maxCap, feeFrequencyType, accountData, taxGroupData);
}
@@ -23,6 +23,7 @@ import org.apache.fineract.portfolio.charge.domain.ChargeAppliesTo;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
public final class ChargeEnumerations {
@@ -191,4 +192,26 @@ public final class ChargeEnumerations {
return optionData;
}
public static EnumOptionData feeFrequencyType(final int id) {
return feeFrequencyType(PeriodFrequencyType.fromInt(id));
}
public static EnumOptionData feeFrequencyType(final PeriodFrequencyType frequencyType) {
EnumOptionData optionData;
switch (frequencyType) {
case DAYS -> optionData = new EnumOptionData(PeriodFrequencyType.DAYS.getValue().longValue(),
PeriodFrequencyType.DAYS.getCode(), "Daily");
case WEEKS -> optionData = new EnumOptionData(PeriodFrequencyType.WEEKS.getValue().longValue(),
PeriodFrequencyType.WEEKS.getCode(), "Weekly");
case MONTHS -> optionData = new EnumOptionData(PeriodFrequencyType.MONTHS.getValue().longValue(),
PeriodFrequencyType.MONTHS.getCode(), "Monthly");
case YEARS -> optionData = new EnumOptionData(PeriodFrequencyType.YEARS.getValue().longValue(),
PeriodFrequencyType.YEARS.getCode(), "Yearly");
case WHOLE_TERM -> optionData = new EnumOptionData(PeriodFrequencyType.WHOLE_TERM.getValue().longValue(),
PeriodFrequencyType.WHOLE_TERM.getCode(), "Whole term");
default -> throw new UnsupportedOperationException(frequencyType + " is not supported");
}
return optionData;
}
}
@@ -43,7 +43,6 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
@@ -52,6 +51,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
@@ -77,6 +77,9 @@ public class LoanChargesApiResource {
"amount", "amountPaid", "chargeOptions", "installmentChargeData"));
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN";
public static final String COMMAND_PAY = "pay";
public static final String COMMAND_WAIVE = "waive";
public static final String COMMAND_ADJUSTMENT = "adjustment";
private final PlatformSecurityContext context;
private final ChargeReadPlatformService chargeReadPlatformService;
@@ -98,10 +101,6 @@ public class LoanChargesApiResource {
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
}
private boolean is(final String commandParam, final String commandValue) {
return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@@ -176,8 +175,8 @@ public class LoanChargesApiResource {
@QueryParam("command") @Parameter(description = "command") final String commandParam,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
CommandProcessingResult result = null;
if (is(commandParam, "pay")) {
CommandProcessingResult result;
if (CommandParameterUtil.is(commandParam, COMMAND_PAY)) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().payLoanCharge(loanId, null).withJson(apiRequestBodyAsJson)
.build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -224,13 +223,16 @@ public class LoanChargesApiResource {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
if (is(commandParam, "waive")) {
CommandProcessingResult result;
if (CommandParameterUtil.is(commandParam, COMMAND_WAIVE)) {
final CommandWrapper commandRequest = builder.waiveLoanCharge(loanId, loanChargeId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "pay")) {
} else if (CommandParameterUtil.is(commandParam, COMMAND_PAY)) {
final CommandWrapper commandRequest = builder.payLoanCharge(loanId, loanChargeId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (CommandParameterUtil.is(commandParam, COMMAND_ADJUSTMENT)) {
final CommandWrapper commandRequest = builder.adjustmentForLoanCharge(loanId, loanChargeId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
@@ -39,7 +39,7 @@ final class LoanChargesApiResourceSwagger {
private GetLoanChargeTimeType() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargeTimeType.disbursement")
public String code;
@Schema(example = "Disbursement")
@@ -51,7 +51,7 @@ final class LoanChargesApiResourceSwagger {
private GetLoanChargeCalculationType() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "chargeCalculationType.flat")
public String code;
@Schema(example = "Flat")
@@ -77,9 +77,9 @@ final class LoanChargesApiResourceSwagger {
}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "1")
public Integer chargeId;
public Long chargeId;
@Schema(example = "Loan Processing fee")
public String name;
public GetLoanChargeTimeType chargeTimeType;
@@ -121,7 +121,7 @@ final class LoanChargesApiResourceSwagger {
private GetLoanChargeTemplateChargeTimeType() {}
@Schema(example = "2")
public Integer id;
public Long id;
@Schema(example = "chargeTimeType.specifiedDueDate")
public String code;
@Schema(example = "Specified due date")
@@ -133,7 +133,7 @@ final class LoanChargesApiResourceSwagger {
private GetLoanChargeTemplateChargeAppliesTo() {}
@Schema(example = "1 ")
public Integer id;
public Long id;
@Schema(example = "chargeAppliesTo.loan")
public String code;
@Schema(example = "Loan")
@@ -141,7 +141,7 @@ final class LoanChargesApiResourceSwagger {
}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "Collection fee")
public String name;
@Schema(example = "true")
@@ -173,7 +173,7 @@ final class LoanChargesApiResourceSwagger {
private PostLoansLoanIdChargesRequest() {}
@Schema(example = "2")
public Integer chargeId;
public Long chargeId;
@Schema(example = "en")
public String locale;
@Schema(example = "100.00")
@@ -198,7 +198,7 @@ final class LoanChargesApiResourceSwagger {
@Schema(example = "1")
public Long loanId;
@Schema(example = "31")
public Integer resourceId;
public Long resourceId;
}
@Schema(description = " PutLoansLoanIdChargesChargeIdRequest")
@@ -228,7 +228,7 @@ final class LoanChargesApiResourceSwagger {
@Schema(example = "1")
public Long loanId;
@Schema(example = "6")
public Integer resourceId;
public Long resourceId;
public PutLoansLoanIdChargesChargeIdRequest changes;
}
@@ -243,6 +243,16 @@ final class LoanChargesApiResourceSwagger {
public String locale;
@Schema(example = "19 September 2013")
public String transactionDate;
@Schema(example = "1")
public Long chargeId;
@Schema(example = "19 September 2013")
public String dueDate;
@Schema(example = "1")
public Long installmentNumber;
@Schema(example = "100.00")
public Double amount;
@Schema(example = "786444UUUYYH7")
public String externalId;
}
@Schema(description = "PostLoansLoanIdChargesChargeIdResponse")
@@ -259,7 +269,26 @@ final class LoanChargesApiResourceSwagger {
@Schema(example = "1")
public Long savingsId;
@Schema(example = "12")
public Integer resourceId;
public Long resourceId;
public PostLoansLoanIdChargesChargeIdChanges changes;
static final class PostLoansLoanIdChargesChargeIdChanges {
@Schema(example = "en")
public String locale;
@Schema(example = "19 September 2013")
public LocalDate transactionDate;
@Schema(example = "1")
public Long chargeId;
@Schema(example = "19 September 2013")
public LocalDate dueDate;
@Schema(example = "1")
public Long installmentNumber;
@Schema(example = "100.00")
public Double amount;
@Schema(example = "786444UUUYYH7")
public String externalId;
}
}
@Schema(description = "DeleteLoansLoanIdChargesChargeIdResponse")
@@ -274,6 +303,6 @@ final class LoanChargesApiResourceSwagger {
@Schema(example = "1")
public Long loanId;
@Schema(example = "2")
public Integer resourceId;
public Long resourceId;
}
}
@@ -39,7 +39,7 @@ final class LoanTransactionsApiResourceSwagger {
private GetLoansTransactionType() {}
@Schema(example = "2")
public Integer id;
public Long id;
@Schema(example = "loanTransactionType.repayment")
public String code;
@Schema(example = "Repayment")
@@ -88,7 +88,7 @@ final class LoanTransactionsApiResourceSwagger {
private GetLoansType() {}
@Schema(example = "2")
public Integer id;
public Long id;
@Schema(example = "loanTransactionType.repayment")
public String code;
@Schema(example = "Repayment")
@@ -137,17 +137,19 @@ final class LoanTransactionsApiResourceSwagger {
public Long fromLoanTransaction;
@Schema(example = "10")
public Long toLoanTransaction;
@Schema(example = "10")
public Long toLoanCharge;
@Schema(example = "CHARGEBACK")
private String relationType;
public String relationType;
@Schema(example = "100.00")
private Double amount;
public Double amount;
@Schema(example = "Repayment Adjustment Chargeback")
private String paymentType;
public String paymentType;
}
@Schema(example = "3")
public Integer id;
public Long id;
public GetLoansType type;
@Schema(example = "[2012, 5, 14]")
public LocalDate date;
@@ -162,6 +164,8 @@ final class LoanTransactionsApiResourceSwagger {
public Double interestPortion;
@Schema(example = "20120514")
public String reversalExternalId;
@Schema(example = "20120514")
public String externalId;
@Schema(example = "[2012, 5, 18]")
public LocalDate reversedOnDate;
@Schema(example = "1000.00")
@@ -201,7 +205,7 @@ final class LoanTransactionsApiResourceSwagger {
@Schema(example = "3e7791ce-aa10-11ec-b909-0242ac120002")
public String externalId;
@Schema(example = "3")
public Integer paymentTypeId;
public Long paymentTypeId;
@Schema(example = "acc123")
public String accountNumber;
@Schema(example = "che123")
@@ -223,11 +227,11 @@ final class LoanTransactionsApiResourceSwagger {
private PostLoansLoanIdTransactionsResponse() {}
@Schema(example = "1")
public Integer officeId;
public Long officeId;
@Schema(example = "1")
public Integer clientId;
public Long clientId;
@Schema(example = "22")
public Integer resourceId;
public Long resourceId;
}
@Schema(description = "PostLoansLoanIdTransactionsTransactionIdRequest")
@@ -248,7 +252,7 @@ final class LoanTransactionsApiResourceSwagger {
@Schema(example = "95174ff9-1a75-4d72-a413-6f9b1cb988b7")
public String reversalExternalId;
@Schema(example = "1")
public Integer paymentTypeId;
public Long paymentTypeId;
@Schema(example = "4ff9b1cb988b7")
public String externalId;
}
@@ -259,7 +263,7 @@ final class LoanTransactionsApiResourceSwagger {
private PostLoansLoanIdTransactionsTransactionIdResponse() {}
@Schema(example = "16")
public Integer resourceId;
public Long resourceId;
}
@Schema(description = "PutChargeTransactionChangesResponse")
@@ -276,9 +280,9 @@ final class LoanTransactionsApiResourceSwagger {
}
@Schema(example = "1")
public Integer resourceId;
public Long resourceId;
@Schema(example = "48")
public Integer loanId;
public Long loanId;
public PutChargeTransactionChangesResponse.Changes changes;
}
@@ -289,8 +293,8 @@ final class LoanTransactionsApiResourceSwagger {
private PutChargeTransactionChangesRequest() {}
@Schema(example = "1")
public Integer id;
public Long id;
@Schema(example = "2")
public Integer loanId;
public Long loanId;
}
}
@@ -267,7 +267,7 @@ final class LoansApiResourceSwagger {
public Double totalPaidLate;
@Schema(example = "0.00")
public Double totalOutstanding;
public Set<GetLoansLoanIdRepaymentPeriod> periods;
public List<GetLoansLoanIdRepaymentPeriod> periods;
}
static final class GetLoansLoanIdRepaymentPeriod {
@@ -635,6 +635,8 @@ final class LoansApiResourceSwagger {
public boolean refundForActiveLoans;
@Schema(example = "false")
public boolean creditBalanceRefund;
@Schema(example = "false")
public boolean chargeAdjustment;
}
static final class GetLoansLoanIdPaymentDetailData {
@@ -964,13 +966,13 @@ final class LoansApiResourceSwagger {
public GetLoansLoanIdSummary summary;
public GetLoansLoanIdRepaymentSchedule repaymentSchedule;
@Schema(description = "Set of GetLoansLoanIdTransactions")
public Set<GetLoansLoanIdTransactions> transactions;
public List<GetLoansLoanIdTransactions> transactions;
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
@Schema(description = "Delinquent data")
public GetLoansLoanIdCollectionData delinquent;
@Schema(description = "Set of charges")
public Set<GetLoansLoanIdLoanChargeData> charges;
public List<GetLoansLoanIdLoanChargeData> charges;
public GetDelinquencyRangesResponse delinquencyRange;
@Schema(example = "false")
public Boolean fraud;
@@ -21,6 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -32,6 +34,8 @@ import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
* Immutable data object for loan charge data.
*/
@Getter
@Builder
@AllArgsConstructor
public class LoanChargeData {
private final Long id;
@@ -20,9 +20,11 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class LoanInstallmentChargeData {
private final Integer installmentNumber;
@@ -32,47 +34,6 @@ public class LoanInstallmentChargeData {
private final BigDecimal amountWaived;
private final boolean paid;
private final boolean waived;
private BigDecimal amountAccrued;
private BigDecimal amountUnrecognized;
public LoanInstallmentChargeData(final Integer installmentNumber, final LocalDate dueDate, final BigDecimal amount,
final BigDecimal amountOutstanding, BigDecimal amountWaived, final boolean paid, final boolean waived) {
this.installmentNumber = installmentNumber;
this.dueDate = dueDate;
this.amount = amount;
this.amountOutstanding = amountOutstanding;
this.paid = paid;
this.waived = waived;
this.amountWaived = amountWaived;
}
public LoanInstallmentChargeData(final Integer installmentNumber, final LocalDate dueDate, final BigDecimal amount,
final BigDecimal amountOutstanding, final BigDecimal amountWaived, final boolean paid, final boolean waived,
final BigDecimal amountAccrued) {
this.installmentNumber = installmentNumber;
this.dueDate = dueDate;
this.amount = amount;
this.amountOutstanding = amountOutstanding;
this.paid = paid;
this.waived = waived;
this.amountWaived = amountWaived;
this.amountAccrued = amountAccrued;
}
public LoanInstallmentChargeData(final LoanInstallmentChargeData installmentChargeData, final BigDecimal amountUnrecognized) {
this.installmentNumber = installmentChargeData.installmentNumber;
this.dueDate = installmentChargeData.dueDate;
this.amount = installmentChargeData.amount;
this.amountOutstanding = installmentChargeData.amountOutstanding;
this.paid = installmentChargeData.paid;
this.waived = installmentChargeData.waived;
this.amountWaived = installmentChargeData.amountWaived;
this.amountAccrued = installmentChargeData.amountAccrued;
this.amountUnrecognized = amountUnrecognized;
}
public boolean isPaymentPending() {
return !(this.paid || this.waived);
}
}
@@ -51,7 +51,7 @@ public class LoanTransactionEnumData {
private final boolean refund;
private final boolean refundForActiveLoans;
private final boolean creditBalanceRefund;
private final boolean chargeAdjustment;
private final boolean chargeback;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
@@ -80,6 +80,7 @@ public class LoanTransactionEnumData {
this.refundForActiveLoans = Long.valueOf(18).equals(this.id);
this.creditBalanceRefund = Long.valueOf(20).equals(this.id);
this.chargeback = Long.valueOf(25).equals(this.id);
this.chargeAdjustment = Long.valueOf(26).equals(this.id);
}
/**
@@ -96,7 +97,8 @@ public class LoanTransactionEnumData {
}
public boolean isRepaymentType() {
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()) {
if (isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|| isChargeAdjustment()) {
return true;
}
return false;
@@ -32,6 +32,7 @@ public class LoanTransactionRelationData implements Serializable {
private Long fromLoanTransaction;
private Long toLoanTransaction;
private Long toLoanCharge;
private LoanTransactionRelationTypeEnum relationType;
private BigDecimal amount;
private String paymentType;
@@ -18,23 +18,20 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.springframework.stereotype.Component;
// TODO: introduce tests for the state machine
@Component
@RequiredArgsConstructor
public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine {
private final List<LoanStatus> allowedLoanStatuses;
private static final List<LoanStatus> ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values());
private final BusinessEventNotifierService businessEventNotifierService;
public DefaultLoanLifecycleStateMachine(LoanStatus[] allowedLoanStatuses, BusinessEventNotifierService businessEventNotifierService) {
this(Arrays.asList(allowedLoanStatuses), businessEventNotifierService);
}
@Override
public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) {
LoanStatus newStatus = getNextStatus(loanEvent, loan);
@@ -164,6 +161,11 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
newState = activeTransition();
}
break;
case LOAN_CHARGE_ADJUSTMENT:
if (from.hasStateOf(LoanStatus.CLOSED_OBLIGATIONS_MET)) {
newState = overpaidTransition();
}
break;
default:
break;
}
@@ -171,47 +173,47 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
}
private LoanStatus transferOnHold() {
return stateOf(LoanStatus.TRANSFER_ON_HOLD, this.allowedLoanStatuses);
return stateOf(LoanStatus.TRANSFER_ON_HOLD, ALLOWED_LOAN_STATUSES);
}
private LoanStatus transferInProgress() {
return stateOf(LoanStatus.TRANSFER_IN_PROGRESS, this.allowedLoanStatuses);
return stateOf(LoanStatus.TRANSFER_IN_PROGRESS, ALLOWED_LOAN_STATUSES);
}
private LoanStatus overpaidTransition() {
return stateOf(LoanStatus.OVERPAID, this.allowedLoanStatuses);
return stateOf(LoanStatus.OVERPAID, ALLOWED_LOAN_STATUSES);
}
private LoanStatus closedRescheduleOutstandingAmountTransition() {
return stateOf(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT, this.allowedLoanStatuses);
return stateOf(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT, ALLOWED_LOAN_STATUSES);
}
private LoanStatus closedWrittenOffTransition() {
return stateOf(LoanStatus.CLOSED_WRITTEN_OFF, this.allowedLoanStatuses);
return stateOf(LoanStatus.CLOSED_WRITTEN_OFF, ALLOWED_LOAN_STATUSES);
}
private LoanStatus closeObligationsMetTransition() {
return stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, this.allowedLoanStatuses);
return stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, ALLOWED_LOAN_STATUSES);
}
private LoanStatus activeTransition() {
return stateOf(LoanStatus.ACTIVE, this.allowedLoanStatuses);
return stateOf(LoanStatus.ACTIVE, ALLOWED_LOAN_STATUSES);
}
private LoanStatus withdrawnByClientTransition() {
return stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, this.allowedLoanStatuses);
return stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, ALLOWED_LOAN_STATUSES);
}
private LoanStatus approvedTransition() {
return stateOf(LoanStatus.APPROVED, this.allowedLoanStatuses);
return stateOf(LoanStatus.APPROVED, ALLOWED_LOAN_STATUSES);
}
private LoanStatus rejectedTransition() {
return stateOf(LoanStatus.REJECTED, this.allowedLoanStatuses);
return stateOf(LoanStatus.REJECTED, ALLOWED_LOAN_STATUSES);
}
private LoanStatus submittedTransition() {
return stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, this.allowedLoanStatuses);
return stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, ALLOWED_LOAN_STATUSES);
}
private LoanStatus stateOf(final LoanStatus state, final List<LoanStatus> allowedLoanStatuses) {
@@ -3379,7 +3379,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private boolean doPostLoanTransactionChecks(final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = false;
if (isOverPaid()) {
boolean isOverpaid = getTotalOverpaid() != null && getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
if (isOverpaid) {
// FIXME - kw - update account balance to negative amount.
handleLoanOverpayment(loanLifecycleStateMachine);
statusChanged = true;
@@ -3620,7 +3621,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (transactionForAdjustment.isNotRepaymentType() && transactionForAdjustment.isNotWaiver()
&& transactionForAdjustment.isNotCreditBalanceRefund()) {
final String errorMessage = "Only transactions of type repayment, waiver or credit balance refund can be adjusted.";
final String errorMessage = "Only (non-reversed) 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);
}
@@ -4935,8 +4936,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
switch (event) {
case LOAN_CREATED:
break;
case LOAN_APPROVED:
if (!isSubmittedAndPendingApproval()) {
final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state.";
@@ -5016,8 +5015,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
dataValidationErrors.add(error);
}
break;
case REPAID_IN_FULL:
break;
case LOAN_CHARGE_PAYMENT:
if (!isOpen()) {
final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active.";
@@ -5082,6 +5079,14 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
dataValidationErrors.add(error);
}
break;
case LOAN_CHARGE_ADJUSTMENT:
if (!(getStatus().isActive() || getStatus().isClosedObligationsMet() || getStatus().isOverpaid())) {
final String defaultUserMessage = "Loan Charge Adjustment is not allowed. Loan Account must be either Active, Fully repaid or Overpaid.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.charge.adjustment.account.is.not.in.valid.state", defaultUserMessage);
dataValidationErrors.add(error);
}
break;
default:
break;
}
@@ -30,7 +30,7 @@ public interface LoanAccountDomainService {
LoanTransaction makeRepayment(LoanTransactionType repaymentTransactionType, Loan loan, CommandProcessingResultBuilder builderResult,
LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId,
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidatDetailDto,
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
Boolean isHolidayValidationDone);
LoanTransaction makeRefund(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate,
@@ -76,6 +76,10 @@ public interface LoanAccountDomainService {
boolean isRecoveryRepayment, String chargeRefundChargeType, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto,
Boolean isHolidayValidationDone, boolean isLoanToLoanTransfer);
void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction);
void saveAndFlushLoanWithDataIntegrityViolationChecks(Loan loan);
void saveLoanWithDataIntegrityViolationChecks(Loan loan);
Map<String, Object> foreCloseLoan(Loan loan, LocalDate foreClourseDate, String noteText);
@@ -123,6 +123,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final PostDatedChecksRepository postDatedChecksRepository;
private final LoanCollateralManagementRepository loanCollateralManagementRepository;
private final DelinquencyWritePlatformService delinquencyWritePlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
@Transactional
@Override
@@ -193,7 +194,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
holidayDetailDto);
final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction,
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment,
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment,
scheduleGeneratorDTO, isHolidayValidationDone);
saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction);
@@ -310,38 +311,23 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
return repaymentEvent;
}
private void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction) {
@Override
public void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction) {
try {
this.loanTransactionRepository.saveAndFlush(newRepaymentTransaction);
} catch (final JpaSystemException | DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").value(newRepaymentTransaction.getExternalId())
.failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors, e);
}
raiseValidationExceptionForUniqueConstraintViolation(e);
throw e;
}
}
private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
@Override
public void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
try {
this.loanRepositoryWrapper.saveAndFlush(loan);
} catch (final JpaSystemException | DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors, e);
}
raiseValidationExceptionForUniqueConstraintViolation(e);
throw e;
}
}
@@ -350,16 +336,22 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
try {
this.loanRepositoryWrapper.save(loan);
} catch (final JpaSystemException | DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors, e);
}
raiseValidationExceptionForUniqueConstraintViolation(e);
throw e;
}
}
private void raiseValidationExceptionForUniqueConstraintViolation(Exception e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
if (realCause.getMessage().toLowerCase().contains("external_id_unique") || realCause.getMessage()
.contains("duplicate key value violates unique constraint \"m_loan_transaction_external_id_key\"")) {
baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors, e);
}
}
@@ -393,7 +385,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
allowTransactionsOnNonWorkingDay);
loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds,
loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds,
holidayDetailDTO, newPaymentTransaction, installmentNumber);
}
saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction);
@@ -427,10 +419,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
private void checkClientOrGroupActive(final Loan loan) {
final Client client = loan.client();
if (client != null) {
@@ -466,7 +454,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds,
loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds,
allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction);
@@ -659,7 +647,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount,
transactionDate, externalId);
loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds,
loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
existingReversedTransactionIds);
this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
@@ -699,7 +687,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled();
loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds,
loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds,
existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
this.loanTransactionRepository.saveAndFlush(newRefundTransaction);
@@ -782,7 +770,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
List<Long> transactionIds = new ArrayList<>();
final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment,
defaultLoanLifecycleStateMachine(), scheduleGeneratorDTO);
defaultLoanLifecycleStateMachine, scheduleGeneratorDTO);
/***
* TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -41,6 +42,7 @@ import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -52,7 +54,9 @@ import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
import org.apache.fineract.portfolio.charge.exception.LoanChargeWithoutMandatoryFieldException;
import org.apache.fineract.portfolio.loanaccount.command.LoanChargeCommand;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
@Entity
@Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") })
@@ -1101,4 +1105,22 @@ public class LoanCharge extends AbstractPersistableCustom {
return ChargeTimeType.fromInt(this.chargeTime);
}
public LoanChargeData toData() {
EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(),
String.valueOf(getChargeTimeType().getValue()));
EnumOptionData chargeCalculationTypeData = new EnumOptionData((long) getChargeCalculation().ordinal(),
getChargeCalculation().getCode(), String.valueOf(getChargeCalculation().getValue()));
EnumOptionData chargePaymentModeData = new EnumOptionData((long) getChargePaymentMode().ordinal(), getChargePaymentMode().getCode(),
String.valueOf(getChargePaymentMode().getValue()));
Set<LoanInstallmentChargeData> loanInstallmentChargeDataSet = installmentCharges().stream().map(LoanInstallmentCharge::toData)
.collect(Collectors.toSet());
return LoanChargeData.builder().id(getId()).chargeId(getCharge().getId()).name(getCharge().getName())
.currency(getCharge().toData().getCurrency()).amount(amount).amountPaid(amountPaid).amountWaived(amountWaived)
.amountWrittenOff(amountWrittenOff).amountOutstanding(amountOutstanding).chargeTimeType(chargeTimeTypeData)
.submittedOnDate(submittedOnDate).dueDate(dueDate).chargeCalculationType(chargeCalculationTypeData).percentage(percentage)
.amountPercentageAppliedTo(amountPercentageAppliedTo).amountOrPercentage(amountOrPercentage).penalty(penaltyCharge)
.chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap)
.installmentChargeData(loanInstallmentChargeDataSet).externalId(externalId).build();
}
}
@@ -50,5 +50,6 @@ public enum LoanEvent {
LOAN_WITHDRAW_TRANSFER, //
LOAN_CREDIT_BALANCE_REFUND, //
LOAN_CHARGEBACK, //
LOAN_CHARGE_ADDED;
LOAN_CHARGE_ADDED, //
LOAN_CHARGE_ADJUSTMENT;
}
@@ -27,6 +27,7 @@ import javax.persistence.Table;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
@Entity
@Table(name = "m_loan_installment_charge")
@@ -322,4 +323,9 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements
public void setInstallment(LoanRepaymentScheduleInstallment installment) {
this.installment = installment;
}
public LoanInstallmentChargeData toData() {
return LoanInstallmentChargeData.builder().installmentNumber(installment.getInstallmentNumber()).dueDate(installment.getDueDate())
.amount(amount).amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).build();
}
}
@@ -172,6 +172,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
chargeRefundChargeType);
}
public static LoanTransaction chargeAdjustment(final Loan loan, final BigDecimal amount, final LocalDate transactionDate,
final String externalId) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, amount, transactionDate, externalId);
}
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) {
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
@@ -296,15 +301,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
public static boolean transactionAmountsMatch(final MonetaryCurrency currency, final LoanTransaction loanTransaction,
final LoanTransaction newLoanTransaction) {
if (loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency))
return loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency))
&& loanTransaction.getPrincipalPortion(currency).isEqualTo(newLoanTransaction.getPrincipalPortion(currency))
&& loanTransaction.getInterestPortion(currency).isEqualTo(newLoanTransaction.getInterestPortion(currency))
&& loanTransaction.getFeeChargesPortion(currency).isEqualTo(newLoanTransaction.getFeeChargesPortion(currency))
&& loanTransaction.getPenaltyChargesPortion(currency).isEqualTo(newLoanTransaction.getPenaltyChargesPortion(currency))
&& loanTransaction.getOverPaymentPortion(currency).isEqualTo(newLoanTransaction.getOverPaymentPortion(currency))) {
return true;
}
return false;
&& loanTransaction.getOverPaymentPortion(currency).isEqualTo(newLoanTransaction.getOverPaymentPortion(currency));
}
private LoanTransaction(final Loan loan, final Office office, final Integer typeOf, final LocalDate dateOf, final BigDecimal amount,
@@ -526,7 +528,8 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
}
public boolean isRepaymentType() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund();
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|| isChargeAdjustment();
}
public boolean isRepayment() {
@@ -549,6 +552,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
return LoanTransactionType.CHARGE_REFUND.equals(getTypeOf()) && isNotReversed();
}
public boolean isChargeAdjustment() {
return LoanTransactionType.CHARGE_ADJUSTMENT.equals(getTypeOf()) && isNotReversed();
}
public boolean isNotRepaymentType() {
return !isRepaymentType();
}
@@ -717,6 +724,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
thisTransactionData.put("loanChargesPaid", loanChargesPaidData);
}
LoanTransactionRelation loanTransactionRelation = loanTransactionRelations.stream()
.filter(e -> LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT.equals(e.getRelationType())).findAny().orElse(null);
if (loanTransactionRelation != null) {
LoanCharge loanCharge = loanTransactionRelation.getToCharge();
thisTransactionData.put("loanChargeData", loanCharge.toData());
}
return thisTransactionData;
}
@@ -874,10 +888,22 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
return (loanTransactionRelations != null && loanTransactionRelations.size() > 0);
}
public boolean hasChargebackLoanTransactionRelations() {
return (loanTransactionRelations != null
&& loanTransactionRelations.stream().anyMatch(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType())));
}
public Set<LoanTransactionRelation> getLoanTransactionRelations() {
return loanTransactionRelations;
}
public void copyLoanTransactionRelations(Set<LoanTransactionRelation> sourceLoanTransactionRelations) {
for (LoanTransactionRelation existingLoanTransactionRelation : sourceLoanTransactionRelations) {
loanTransactionRelations.add(new LoanTransactionRelation(this, existingLoanTransactionRelation.getToTransaction(),
existingLoanTransactionRelation.getToCharge(), existingLoanTransactionRelation.getRelationType()));
}
}
public BigDecimal getAmount() {
return amount;
}
@@ -23,6 +23,7 @@ import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Version;
import javax.validation.constraints.NotNull;
@@ -36,12 +37,18 @@ import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
@Table(name = "m_loan_transaction_relation")
public class LoanTransactionRelation extends AbstractAuditableWithUTCDateTimeCustom {
@ManyToOne
@JoinColumn(name = "from_loan_transaction_id", nullable = false)
private LoanTransaction fromTransaction;
@JoinColumn(name = "to_loan_transaction_id", nullable = false)
@ManyToOne
@JoinColumn(name = "to_loan_transaction_id")
private LoanTransaction toTransaction;
@ManyToOne
@JoinColumn(name = "to_loan_charge_id")
private LoanCharge toCharge;
@Enumerated(EnumType.ORDINAL)
@Column(name = "relation_type_enum", nullable = false)
private LoanTransactionRelationTypeEnum relationType;
@@ -51,15 +58,21 @@ public class LoanTransactionRelation extends AbstractAuditableWithUTCDateTimeCus
protected LoanTransactionRelation() {}
protected LoanTransactionRelation(@NotNull LoanTransaction fromTransaction, @NotNull LoanTransaction toTransaction,
protected LoanTransactionRelation(@NotNull LoanTransaction fromTransaction, LoanTransaction toTransaction, LoanCharge toCharge,
LoanTransactionRelationTypeEnum relationType) {
this.fromTransaction = fromTransaction;
this.toTransaction = toTransaction;
this.toCharge = toCharge;
this.relationType = relationType;
}
public static LoanTransactionRelation instance(@NotNull LoanTransaction fromTransaction, @NotNull LoanTransaction toTransaction,
public static LoanTransactionRelation linkToTransaction(@NotNull LoanTransaction fromTransaction,
@NotNull LoanTransaction toTransaction, LoanTransactionRelationTypeEnum relation) {
return new LoanTransactionRelation(fromTransaction, toTransaction, null, relation);
}
public static LoanTransactionRelation linkToCharge(@NotNull LoanTransaction fromTransaction, @NotNull LoanCharge loanCharge,
LoanTransactionRelationTypeEnum relation) {
return new LoanTransactionRelation(fromTransaction, toTransaction, relation);
return new LoanTransactionRelation(fromTransaction, null, loanCharge, relation);
}
}
@@ -25,7 +25,6 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanTransactionRelationRepository
extends JpaRepository<LoanTransactionRelation, Long>, JpaSpecificationExecutor<LoanTransactionRelation> {
List<LoanTransactionRelation> findByFromTransactionAndRelationType(LoanTransaction loanTransaction,
LoanTransactionRelationTypeEnum relationType);
List<LoanTransactionRelation> findByFromTransaction(LoanTransaction fromTransaction);
}
@@ -21,7 +21,8 @@ package org.apache.fineract.portfolio.loanaccount.domain;
public enum LoanTransactionRelationTypeEnum {
INVALID(0, "loanTransactionType.invalid"), //
CHARGEBACK(1, "loanTransactionRelationType.chargeback");
CHARGEBACK(1, "loanTransactionRelationType.chargeback"), //
CHARGE_ADJUSTMENT(2, "loanTransactionRelationType.chargeAdjustment");
private final Integer value;
private final String code;
@@ -47,6 +48,7 @@ public enum LoanTransactionRelationTypeEnum {
return switch (transactionType) {
case 1 -> LoanTransactionRelationTypeEnum.CHARGEBACK;
case 2 -> LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT;
default -> LoanTransactionRelationTypeEnum.INVALID;
};
}
@@ -57,7 +57,8 @@ public enum LoanTransactionType {
PAYOUT_REFUND(22, "loanTransactionType.payoutRefund"), //
GOODWILL_CREDIT(23, "loanTransactionType.goodwillCredit"), //
CHARGE_REFUND(24, "loanTransactionType.chargeRefund"), //
CHARGEBACK(25, "loanTransactionType.chargeback");
CHARGEBACK(25, "loanTransactionType.chargeback"), //
CHARGE_ADJUSTMENT(26, "loanTransactionType.chargeAdjustment");
private final Integer value;
private final String code;
@@ -67,14 +68,6 @@ public enum LoanTransactionType {
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public static LoanTransactionType fromInt(final Integer transactionType) {
if (transactionType == null) {
@@ -106,36 +99,45 @@ public enum LoanTransactionType {
case 23 -> LoanTransactionType.GOODWILL_CREDIT;
case 24 -> LoanTransactionType.CHARGE_REFUND;
case 25 -> LoanTransactionType.CHARGEBACK;
case 26 -> LoanTransactionType.CHARGE_ADJUSTMENT;
default -> LoanTransactionType.INVALID;
};
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public boolean isDisbursement() {
return this.value.equals(LoanTransactionType.DISBURSEMENT.getValue());
return this.equals(LoanTransactionType.DISBURSEMENT);
}
public boolean isRepaymentAtDisbursement() {
return this.value.equals(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue());
return this.equals(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT);
}
public boolean isRepayment() {
return this.value.equals(LoanTransactionType.REPAYMENT.getValue());
return this.equals(LoanTransactionType.REPAYMENT);
}
public boolean isMerchantIssuedRefund() {
return this.value.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue());
return this.equals(LoanTransactionType.MERCHANT_ISSUED_REFUND);
}
public boolean isPayoutRefund() {
return this.value.equals(LoanTransactionType.PAYOUT_REFUND.getValue());
return this.equals(LoanTransactionType.PAYOUT_REFUND);
}
public boolean isGoodwillCredit() {
return this.value.equals(LoanTransactionType.GOODWILL_CREDIT.getValue());
return this.equals(LoanTransactionType.GOODWILL_CREDIT);
}
public boolean isChargeRefund() {
return this.value.equals(LoanTransactionType.CHARGE_REFUND.getValue());
return this.equals(LoanTransactionType.CHARGE_REFUND);
}
public boolean isRepaymentType() {
@@ -143,39 +145,43 @@ public enum LoanTransactionType {
}
public boolean isRecoveryRepayment() {
return this.value.equals(LoanTransactionType.RECOVERY_REPAYMENT.getValue());
return this.equals(LoanTransactionType.RECOVERY_REPAYMENT);
}
public boolean isWaiveInterest() {
return this.value.equals(LoanTransactionType.WAIVE_INTEREST.getValue());
return this.equals(LoanTransactionType.WAIVE_INTEREST);
}
public boolean isWaiveCharges() {
return this.value.equals(LoanTransactionType.WAIVE_CHARGES.getValue());
return this.equals(LoanTransactionType.WAIVE_CHARGES);
}
public boolean isAccrual() {
return this.value.equals(LoanTransactionType.ACCRUAL.getValue());
return this.equals(LoanTransactionType.ACCRUAL);
}
public boolean isWriteOff() {
return this.value.equals(LoanTransactionType.WRITEOFF.getValue());
return this.equals(LoanTransactionType.WRITEOFF);
}
public boolean isChargePayment() {
return this.value.equals(LoanTransactionType.CHARGE_PAYMENT.getValue());
return this.equals(LoanTransactionType.CHARGE_PAYMENT);
}
public boolean isRefundForActiveLoan() {
return this.value.equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN.getValue());
return this.equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN);
}
public boolean isIncomePosting() {
return this.value.equals(LoanTransactionType.INCOME_POSTING.getValue());
return this.equals(LoanTransactionType.INCOME_POSTING);
}
public boolean isChargeback() {
return this.value.equals(LoanTransactionType.CHARGEBACK.getValue());
return this.equals(LoanTransactionType.CHARGEBACK);
}
public boolean isChargeAdjustment() {
return this.equals(LoanTransactionType.CHARGE_ADJUSTMENT);
}
}
@@ -185,6 +185,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} else {
loanTransaction.reverse(loanTransaction.getExternalId());
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(), newLoanTransaction);
}
}
@@ -0,0 +1,29 @@
/**
* 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.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
public class LoanChargeAdjustmentException extends AbstractPlatformDomainRuleException {
public LoanChargeAdjustmentException(final String errorCode, final String defaultUserMessage, final Object... defaultUserMessageArgs) {
super("error.msg." + errorCode, defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -24,7 +24,7 @@ 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.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
@@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOANCHARGE", action = "CREATE")
public class AddLoanChargeCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final LoanChargeWritePlatformService writePlatformService;
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
@Transactional
@@ -23,7 +23,7 @@ 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.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOANCHARGE", action = "DELETE")
public class DeleteLoanChargeCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final LoanChargeWritePlatformService writePlatformService;
@Transactional
@Override
@@ -0,0 +1,42 @@
/**
* 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.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOANCHARGE", action = "ADJUSTMENT")
public class LoanChargeAdjustmentCommandHandler implements NewCommandSourceHandler {
private final LoanChargeWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return writePlatformService.adjustmentForLoanCharge(command.getLoanId(), command.entityId(), command);
}
}
@@ -24,7 +24,7 @@ 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.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
@@ -35,7 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOAN", action = "CHARGEREFUND")
public class LoanChargeRefundCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final LoanChargeWritePlatformService writePlatformService;
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
@Transactional
@@ -23,7 +23,7 @@ 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.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOANCHARGE", action = "PAY")
public class PayLoanChargeCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final LoanChargeWritePlatformService writePlatformService;
@Transactional
@Override
@@ -23,7 +23,7 @@ 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.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "WAIVECHARGE", action = "UNDO")
public class UndoLoanChargeWaiveCommandHandler implements NewCommandSourceHandler {
final LoanWritePlatformService loanWritePlatformService;
final LoanChargeWritePlatformService loanWritePlatformService;
@Transactional
@Override
@@ -23,7 +23,7 @@ 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.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOANCHARGE", action = "UPDATE")
public class UpdateLoanChargeCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final LoanChargeWritePlatformService writePlatformService;
@Transactional
@Override
@@ -23,7 +23,7 @@ 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.portfolio.loanaccount.service.LoanWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,7 +32,7 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "LOANCHARGE", action = "WAIVE")
public class WaiveLoanChargeCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final LoanChargeWritePlatformService writePlatformService;
@Transactional
@Override
@@ -20,8 +20,8 @@ package org.apache.fineract.portfolio.loanaccount.jobs.applychargetooverdueloani
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
@@ -44,7 +44,7 @@ public class ApplyChargeToOverdueLoanInstallmentConfig {
@Autowired
private LoanReadPlatformService loanReadPlatformService;
@Autowired
private LoanWritePlatformService loanWritePlatformService;
private LoanChargeWritePlatformService loanChargeWritePlatformService;
@Bean
protected Step applyChargeToOverdueLoanInstallmentStep() {
@@ -61,6 +61,6 @@ public class ApplyChargeToOverdueLoanInstallmentConfig {
@Bean
public ApplyChargeToOverdueLoanInstallmentTasklet applyChargeToOverdueLoanInstallmentTasklet() {
return new ApplyChargeToOverdueLoanInstallmentTasklet(configurationDomainService, loanReadPlatformService,
loanWritePlatformService);
loanChargeWritePlatformService);
}
}
@@ -31,8 +31,8 @@ import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainR
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
@@ -44,7 +44,7 @@ public class ApplyChargeToOverdueLoanInstallmentTasklet implements Tasklet {
private final ConfigurationDomainService configurationDomainService;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanWritePlatformService loanWritePlatformService;
private final LoanChargeWritePlatformService loanChargeWritePlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -68,7 +68,7 @@ public class ApplyChargeToOverdueLoanInstallmentTasklet implements Tasklet {
List<Throwable> exceptions = new ArrayList<>();
for (final Long loanId : overdueScheduleData.keySet()) {
try {
loanWritePlatformService.applyOverdueChargesForLoan(loanId, overdueScheduleData.get(loanId));
loanChargeWritePlatformService.applyOverdueChargesForLoan(loanId, overdueScheduleData.get(loanId));
} catch (final PlatformApiDataValidationException e) {
final List<ApiParameterError> errors = e.getErrors();
@@ -30,6 +30,7 @@ public interface LoanTransactionRelationMapper {
@Mapping(target = "fromLoanTransaction", source = "source.fromTransaction.id")
@Mapping(target = "toLoanTransaction", source = "source.toTransaction.id")
@Mapping(target = "toLoanCharge", source = "source.toCharge.id")
@Mapping(target = "amount", source = "source.toTransaction.amount")
@Mapping(target = "paymentType", source = "source.toTransaction.paymentDetail.paymentType.name")
LoanTransactionRelationData map(LoanTransactionRelation source);
@@ -47,13 +47,10 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollatera
import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollateralQuantity;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
@@ -107,7 +104,7 @@ public final class LoanApplicationCommandFromApiJsonHelper {
private final FromJsonHelper fromApiJsonHelper;
private final CalculateLoanScheduleQueryFromApiJsonHelper apiJsonHelper;
private final ClientCollateralManagementRepositoryWrapper clientCollateralManagementRepositoryWrapper;
private final LoanChargeApiJsonValidator loanChargeApiJsonValidator;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
public void validateForCreate(final String json, final boolean isMeetingMandatoryForJLGLoans, final LoanProduct loanProduct) {
@@ -391,15 +388,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName)
.value(transactionProcessingStrategy).notNull();
LoanRepaymentScheduleTransactionProcessor processor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(transactionProcessingStrategy);
if (processor == null) {
Object[] values = loanRepaymentScheduleTransactionProcessorFactory.getStrategies().stream()
.map(TransactionProcessingStrategyData::getCode).toList().toArray(new String[0]);
baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName).isOneOfTheseValues(values);
}
// Validating whether the processor is existing
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy);
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.linkAccountIdParameterName, element)) {
final Long linkAccountId = this.fromApiJsonHelper.extractLongNamed(LoanApiConstants.linkAccountIdParameterName, element);
@@ -640,16 +630,8 @@ public final class LoanApplicationCommandFromApiJsonHelper {
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName)
.value(transactionProcessingStrategy).notNull();
LoanRepaymentScheduleTransactionProcessor processor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(transactionProcessingStrategy);
if (processor == null) {
Object[] values = loanRepaymentScheduleTransactionProcessorFactory.getStrategies().stream()
.map(TransactionProcessingStrategyData::getCode).toList().toArray(new String[0]);
baseDataValidator.reset().parameter(LoanApiConstants.transactionProcessingStrategyCodeParameterName)
.isOneOfTheseValues(values);
}
// Validating whether the processor is existing
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategy);
}
BigDecimal principal = null;
@@ -1282,46 +1264,10 @@ public final class LoanApplicationCommandFromApiJsonHelper {
}
public void validateLoanCharges(final Set<LoanCharge> charges, final List<ApiParameterError> dataValidationErrors) {
if (charges == null) {
return;
}
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
for (LoanCharge loanCharge : charges) {
String errorcode = null;
switch (loanCharge.getChargeCalculation()) {
case PERCENT_OF_AMOUNT:
if (loanCharge.isInstalmentFee()) {
errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE;
}
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
if (loanCharge.isInstalmentFee()) {
errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE;
} else if (loanCharge.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
}
break;
case PERCENT_OF_INTEREST:
if (loanCharge.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
}
break;
default:
break;
}
if (errorcode != null) {
baseDataValidator.reset().parameter("charges").failWithCode(errorcode);
}
}
}
public void validateLoanForInterestRecalculation(final Loan loan) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
validateLoanCharges(loan.getActiveCharges(), dataValidationErrors);
loanChargeApiJsonValidator.validateLoanCharges(loan.getActiveCharges(), dataValidationErrors);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
@@ -0,0 +1,271 @@
/**
* 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.serialization;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
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.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.springframework.stereotype.Component;
@Component
@AllArgsConstructor
public final class LoanChargeApiJsonValidator {
private final FromJsonHelper fromApiJsonHelper;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
public void validateAddLoanCharge(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> disbursementParameters = new HashSet<>(
Arrays.asList("chargeId", "amount", "dueDate", "locale", "dateFormat", "externalId"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element);
baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero();
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element);
baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount();
if (this.fromApiJsonHelper.parameterExists("dueDate", element)) {
final LocalDate dueDate = this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element);
baseDataValidator.reset().parameter("dueDate").value(dueDate).notBlank();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateUpdateOfLoanCharge(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> disbursementParameters = new HashSet<>(Arrays.asList("amount", "dueDate", "locale", "dateFormat"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element);
baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount();
if (this.fromApiJsonHelper.parameterExists("dueDate", element)) {
this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateChargePaymentTransaction(final String json, final boolean isChargeIdIncluded) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
Set<String> transactionParameters = null;
if (isChargeIdIncluded) {
transactionParameters = new HashSet<>(
Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId", "dueDate", "installmentNumber"));
} else {
transactionParameters = new HashSet<>(Arrays.asList("transactionDate", "locale", "dateFormat", "dueDate", "installmentNumber"));
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.payment.transaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
if (isChargeIdIncluded) {
final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element);
baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero();
}
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element);
baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateLoanChargeRefundTransaction(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final String loanChargeIdParam = "loanChargeId";
final String installmentNumberParam = "installmentNumber";
final String dueDateParam = "dueDate";
Set<String> transactionParameters = new HashSet<>(
Arrays.asList(loanChargeIdParam, dueDateParam, "locale", "dateFormat", installmentNumberParam, //
// remainder below relate to payment part of refund and not validated here
"transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber",
"routingCode", "receiptNumber", "bankNumber"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.refund.transaction");
JsonElement element = this.fromApiJsonHelper.parse(json);
final Long chargeId = this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element);
baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero();
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, element);
baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateLoanAdjustmentRequest(final Long loanId, final Long loanChargeId, final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
Set<String> transactionParameters = new HashSet<>(List.of("amount", "externalId", "locale"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.adjustment.request");
JsonElement element = this.fromApiJsonHelper.parse(json);
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element);
baseDataValidator.reset().parameter("amount").value(amount).ignoreIfNull().positiveAmount();
baseDataValidator.reset().parameter("loanId").value(loanId).notNull().positiveAmount();
baseDataValidator.reset().parameter("loanChargeId").value(loanChargeId).notNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateInstallmentChargeTransaction(final String json) {
if (StringUtils.isBlank(json)) {
return;
}
Set<String> transactionParameters = new HashSet<>(Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.waive.transaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element);
baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateLoanCharges(final Set<LoanCharge> charges, final List<ApiParameterError> dataValidationErrors) {
if (charges == null) {
return;
}
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
for (LoanCharge loanCharge : charges) {
String errorcode = null;
switch (loanCharge.getChargeCalculation()) {
case PERCENT_OF_AMOUNT:
if (loanCharge.isInstalmentFee()) {
errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE;
}
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
if (loanCharge.isInstalmentFee()) {
errorcode = "installment." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_PRINCIPAL_CALCULATION_TYPE;
} else if (loanCharge.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
}
break;
case PERCENT_OF_INTEREST:
if (loanCharge.isSpecifiedDueDate()) {
errorcode = "specific." + LoanApiConstants.LOAN_CHARGE_CAN_NOT_BE_ADDED_WITH_INTEREST_CALCULATION_TYPE;
}
break;
default:
break;
}
if (errorcode != null) {
baseDataValidator.reset().parameter("charges").failWithCode(errorcode);
}
}
}
}
@@ -338,60 +338,6 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateAddLoanCharge(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> disbursementParameters = new HashSet<>(
Arrays.asList("chargeId", "amount", "dueDate", "locale", "dateFormat", "externalId"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element);
baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero();
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element);
baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount();
if (this.fromApiJsonHelper.parameterExists("dueDate", element)) {
final LocalDate dueDate = this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element);
baseDataValidator.reset().parameter("dueDate").value(dueDate).notBlank();
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateUpdateOfLoanCharge(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Set<String> disbursementParameters = new HashSet<>(Arrays.asList("amount", "dueDate", "locale", "dateFormat"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loanCharge");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final BigDecimal amount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("amount", element);
baseDataValidator.reset().parameter("amount").value(amount).notNull().positiveAmount();
if (this.fromApiJsonHelper.parameterExists("dueDate", element)) {
this.fromApiJsonHelper.extractLocalDateNamed("dueDate", element);
}
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateUpdateOfLoanOfficer(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -450,74 +396,6 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateChargePaymentTransaction(final String json, final boolean isChargeIdIncluded) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
Set<String> transactionParameters = null;
if (isChargeIdIncluded) {
transactionParameters = new HashSet<>(
Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId", "dueDate", "installmentNumber"));
} else {
transactionParameters = new HashSet<>(Arrays.asList("transactionDate", "locale", "dateFormat", "dueDate", "installmentNumber"));
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.payment.transaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
if (isChargeIdIncluded) {
final Long chargeId = this.fromApiJsonHelper.extractLongNamed("chargeId", element);
baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero();
}
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element);
baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateLoanChargeRefundTransaction(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final String loanChargeIdParam = "loanChargeId";
final String installmentNumberParam = "installmentNumber";
final String dueDateParam = "dueDate";
Set<String> transactionParameters = new HashSet<>(
Arrays.asList(loanChargeIdParam, dueDateParam, "locale", "dateFormat", installmentNumberParam, //
// remainder below relate to payment part of refund and not validated here
"transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber",
"routingCode", "receiptNumber", "bankNumber"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.refund.transaction");
JsonElement element = this.fromApiJsonHelper.parse(json);
final Long chargeId = this.fromApiJsonHelper.extractLongNamed(loanChargeIdParam, element);
baseDataValidator.reset().parameter(loanChargeIdParam).value(chargeId).notNull().integerGreaterThanZero();
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(installmentNumberParam, element);
baseDataValidator.reset().parameter(installmentNumberParam).value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).ignoreIfNull().positiveAmount();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateMarkAsFraudLoan(final String json) {
if (StringUtils.isBlank(json)) {
return;
@@ -539,27 +417,6 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateInstallmentChargeTransaction(final String json) {
if (StringUtils.isBlank(json)) {
return;
}
Set<String> transactionParameters = new HashSet<>(Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource("loan.charge.waive.transaction");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element);
baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero();
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
public void validateUpdateDisbursementDateAndAmount(final String json, LoanDisbursementDetails loanDisbursementDetails) {
if (StringUtils.isBlank(json)) {
@@ -98,7 +98,6 @@ import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -192,10 +191,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final GLIMAccountInfoRepository glimRepository;
private final LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService;
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
@Transactional
@Override
@@ -1355,7 +1351,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
}
final Map<String, Object> changes = loan.loanApplicationApproval(currentUser, command, disbursementDataArray,
defaultLoanLifecycleStateMachine());
defaultLoanLifecycleStateMachine);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.APPROVE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
@@ -1462,7 +1458,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
final Loan loan = retrieveLoanBy(loanId);
checkClientOrGroupActive(loan);
final Map<String, Object> changes = loan.undoApproval(defaultLoanLifecycleStateMachine());
final Map<String, Object> changes = loan.undoApproval(defaultLoanLifecycleStateMachine);
if (!changes.isEmpty()) {
// If loan approved amount is not same as loan amount demanded, then
@@ -1544,7 +1540,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.REJECTED.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
final Map<String, Object> changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine());
final Map<String, Object> changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine);
if (!changes.isEmpty()) {
this.loanRepositoryWrapper.saveAndFlush(loan);
@@ -1581,7 +1577,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
StatusEnum.WITHDRAWN.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
final Map<String, Object> changes = loan.loanApplicationWithdrawnByApplicant(currentUser, command,
defaultLoanLifecycleStateMachine());
defaultLoanLifecycleStateMachine);
// Release attached collaterals
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
@@ -1619,7 +1615,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private Loan retrieveLoanBy(final Long loanId) {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
loan.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory);
loan.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory);
return loan;
}
@@ -34,7 +34,6 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
@@ -60,7 +59,6 @@ import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.group.exception.GroupNotFoundException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
@@ -68,7 +66,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidAmountOfCollaterals;
@@ -109,18 +106,18 @@ public class LoanAssembler {
private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final LoanUtilService loanUtilService;
private final RateAssembler rateAssembler;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
public Loan assembleFrom(final Long accountId) {
final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true);
loanAccount.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper,
loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
this.loanRepaymentScheduleTransactionProcessorFactory);
return loanAccount;
}
public void setHelpers(final Loan loanAccount) {
loanAccount.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper,
loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
this.loanRepaymentScheduleTransactionProcessorFactory);
}
@@ -296,7 +293,7 @@ public class LoanAssembler {
if (loanApplication == null) {
throw new IllegalStateException("No loan application exists for either a client or group (or both).");
}
loanApplication.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper,
loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
this.loanRepaymentScheduleTransactionProcessorFactory);
if (loanProduct.isMultiDisburseLoan()) {
@@ -314,16 +311,12 @@ public class LoanAssembler {
final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled();
final LoanScheduleModel loanScheduleModel = this.loanScheduleAssembler.assembleLoanScheduleFrom(loanApplicationTerms,
isHolidayEnabled, holidays, workingDays, element, disbursementDetails);
loanApplication.loanApplicationSubmittal(loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine(),
submittedOnDate, externalId, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
loanApplication.loanApplicationSubmittal(loanScheduleModel, loanApplicationTerms, defaultLoanLifecycleStateMachine, submittedOnDate,
externalId, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay);
return loanApplication;
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
return new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
}
public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) {
CodeValue codeValue = null;
if (codeValueId != null) {
@@ -43,6 +43,6 @@ public interface LoanChargeReadPlatformService {
Collection<LoanChargeData> retrieveLoanChargesForAccural(Long loanId);
Collection<LoanChargePaidByData> retriveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType,
Collection<LoanChargePaidByData> retrieveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType,
Integer installmentNumber);
}
@@ -65,7 +65,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
+ "lc.calculation_percentage as percentageOf, lc.calculation_on_amount as amountPercentageAppliedTo, "
+ "lc.charge_time_enum as chargeTime, " + "lc.is_penalty as penalty, "
+ "lc.due_for_collection_as_of_date as dueAsOfDate, " + "lc.charge_calculation_enum as chargeCalculation, "
+ "lc.charge_payment_mode_enum as chargePaymentMode, " + "lc.is_paid_derived as paid, " + "lc.waived as waied, "
+ "lc.charge_payment_mode_enum as chargePaymentMode, " + "lc.is_paid_derived as paid, " + "lc.waived as waived, "
+ "lc.min_cap as minCap, lc.max_cap as maxCap, " + "lc.charge_amount_or_percentage as amountOrPercentage, "
+ "lc.loan_id as loanId, c.currency_code as currencyCode, oc.name as currencyName, "
+ "date(coalesce(dd.disbursedon_date,dd.expected_disburse_date)) as disbursementDate, "
@@ -112,7 +112,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
final int chargePaymentMode = rs.getInt("chargePaymentMode");
final EnumOptionData paymentMode = ChargeEnumerations.chargePaymentMode(chargePaymentMode);
final boolean paid = rs.getBoolean("paid");
final boolean waived = rs.getBoolean("waied");
final boolean waived = rs.getBoolean("waived");
final BigDecimal minCap = rs.getBigDecimal("minCap");
final BigDecimal maxCap = rs.getBigDecimal("maxCap");
final BigDecimal amountOrPercentage = rs.getBigDecimal("amountOrPercentage");
@@ -227,7 +227,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
public String schema() {
return " lsi.installment as installmentNumber, lsi.duedate as dueAsOfDate, "
+ "lic.amount_outstanding_derived as amountOutstanding," + "lic.amount as amount, " + "lic.is_paid_derived as paid, "
+ "lic.amount_waived_derived as amountWaived, " + "lic.waived as waied " + "from m_loan_installment_charge lic "
+ "lic.amount_waived_derived as amountWaived, " + "lic.waived as waived " + "from m_loan_installment_charge lic "
+ "join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id ";
}
@@ -239,9 +239,10 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
final BigDecimal amount = rs.getBigDecimal("amount");
final BigDecimal amountWaived = rs.getBigDecimal("amountWaived");
final boolean paid = rs.getBoolean("paid");
final boolean waived = rs.getBoolean("waied");
final boolean waived = rs.getBoolean("waived");
return new LoanInstallmentChargeData(installmentNumber, dueAsOfDate, amount, amountOutstanding, amountWaived, paid, waived);
return LoanInstallmentChargeData.builder().installmentNumber(installmentNumber).dueDate(dueAsOfDate).amount(amount)
.amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).build();
}
}
@@ -419,7 +420,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
sb.append("lic.is_paid_derived as paid, ");
sb.append("lic.amount_waived_derived as amountWaived, ");
sb.append(" sum(cp.amount) as amountAccrued, ");
sb.append("lic.waived as waied ");
sb.append("lic.waived as waived ");
sb.append("from m_loan_installment_charge lic ");
sb.append("join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id ");
sb.append("left join (");
@@ -443,11 +444,12 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
final BigDecimal amount = rs.getBigDecimal("amount");
final BigDecimal amountWaived = rs.getBigDecimal("amountWaived");
final boolean paid = rs.getBoolean("paid");
final boolean waived = rs.getBoolean("waied");
final boolean waived = rs.getBoolean("waived");
final BigDecimal amountAccrued = rs.getBigDecimal("amountAccrued");
return new LoanInstallmentChargeData(installmentNumber, dueAsOfDate, amount, amountOutstanding, amountWaived, paid, waived,
amountAccrued);
return LoanInstallmentChargeData.builder().installmentNumber(installmentNumber).dueDate(dueAsOfDate).amount(amount)
.amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).amountAccrued(amountAccrued)
.build();
}
}
@@ -483,12 +485,16 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
final Integer installmentNumber = rs.getInt("installmentNumber");
final BigDecimal amountUnrecognized = rs.getBigDecimal("amountUnrecognized");
LoanInstallmentChargeData installmentChargeData = this.installmentChargeDatas.get(installmentNumber);
return new LoanInstallmentChargeData(installmentChargeData, amountUnrecognized);
return LoanInstallmentChargeData.builder().installmentNumber(installmentChargeData.getInstallmentNumber())
.dueDate(installmentChargeData.getDueDate()).amount(installmentChargeData.getAmount())
.amountOutstanding(installmentChargeData.getAmountOutstanding()).amountWaived(installmentChargeData.getAmountWaived())
.paid(installmentChargeData.isPaid()).waived(installmentChargeData.isWaived())
.amountAccrued(installmentChargeData.getAmountAccrued()).amountUnrecognized(amountUnrecognized).build();
}
}
@Override
public Collection<LoanChargePaidByData> retriveLoanChargesPaidBy(Long chargeId, final LoanTransactionType transactionType,
public Collection<LoanChargePaidByData> retrieveLoanChargesPaidBy(Long chargeId, final LoanTransactionType transactionType,
final Integer installmentNumber) {
LoanChargesPaidByMapper rm = new LoanChargesPaidByMapper();
@@ -0,0 +1,45 @@
/**
* 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.util.Collection;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
public interface LoanChargeWritePlatformService {
CommandProcessingResult addLoanCharge(Long loanId, JsonCommand command);
CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command);
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
CommandProcessingResult updateLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult deleteLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult payLoanCharge(Long loanId, Long loanChargeId, JsonCommand command, boolean isChargeIdIncludedInJson);
CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
void applyOverdueChargesForLoan(Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDataList);
}
@@ -110,7 +110,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
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.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
@@ -264,11 +263,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final String sql = "select " + rm.loanPaymentsSchema() + " where tr.loan_id = ? and tr.transaction_type_enum not in (0, 3) "
+ " and (tr.is_reversed=false or tr.manually_adjusted_or_reversed = true) order by tr.transaction_date ASC,id ";
Collection<LoanTransactionData> loanTransactionData = this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR
// TODO: would worth to rework in the future. It is not nice to fetch relations one by one... might worth to
// give a try to get rid of native queries
for (LoanTransactionData loanTransaction : loanTransactionData) {
if (loanTransaction.getType().isRepaymentType()) {
loanTransaction
.setLoanTransactionRelations(this.retrieveLoanTransactionRelationsByLoanTransactionId(loanTransaction.getId()));
}
loanTransaction
.setLoanTransactionRelations(this.retrieveLoanTransactionRelationsByLoanTransactionId(loanTransaction.getId()));
}
return loanTransactionData;
} catch (final EmptyResultDataAccessException e) {
@@ -2399,7 +2398,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
public List<LoanTransactionRelationData> retrieveLoanTransactionRelationsByLoanTransactionId(Long loanTransactionId) {
final LoanTransaction loanTransaction = this.loanTransactionRepository.getReferenceById(loanTransactionId);
List<LoanTransactionRelation> loanTransactionRelations = this.loanTransactionRelationRepository
.findByFromTransactionAndRelationType(loanTransaction, LoanTransactionRelationTypeEnum.CHARGEBACK);
.findByFromTransaction(loanTransaction);
return loanTransactionRelationMapper.map(loanTransactionRelations);
}
@@ -32,7 +32,7 @@ import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulk
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.loanaccount.loanschedule.data.OverdueLoanScheduleData;
import org.springframework.transaction.annotation.Transactional;
public interface LoanWritePlatformService {
@@ -46,6 +46,10 @@ public interface LoanWritePlatformService {
CommandProcessingResult makeLoanRepayment(LoanTransactionType repaymentTransactionType, Long loanId, JsonCommand command,
boolean isRecoveryRepayment);
@Transactional
CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId,
JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType);
Map<String, Object> makeLoanBulkRepayment(CollectionSheetBulkRepaymentCommand bulkRepaymentCommand);
CommandProcessingResult adjustLoanTransaction(Long loanId, Long transactionId, JsonCommand command);
@@ -60,18 +64,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult closeAsRescheduled(Long loanId, JsonCommand command);
CommandProcessingResult addLoanCharge(Long loanId, JsonCommand command);
CommandProcessingResult updateLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult deleteLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult waiveLoanCharge(Long loanId, Long loanChargeId, JsonCommand command);
CommandProcessingResult undoWaiveLoanCharge(JsonCommand command);
CommandProcessingResult loanChargeRefund(Long loanId, JsonCommand command);
CommandProcessingResult loanReassignment(Long loanId, JsonCommand command);
CommandProcessingResult bulkLoanReassignment(JsonCommand command);
@@ -89,8 +81,6 @@ public interface LoanWritePlatformService {
LoanTransaction acceptLoanTransfer(Loan loan, LocalDate transferDate, Office acceptedInOffice, Staff loanOfficer);
CommandProcessingResult payLoanCharge(Long loanId, Long loanChargeId, JsonCommand command, boolean isChargeIdIncludedInJson);
CommandProcessingResult undoWriteOff(Long loanId);
CommandProcessingResult updateDisbursementDateAndAmountForTranche(Long loanId, Long disbursementId, JsonCommand command);
@@ -103,8 +93,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult addAndDeleteLoanDisburseDetails(Long loanId, JsonCommand command);
void applyOverdueChargesForLoan(Long loanId, Collection<OverdueLoanScheduleData> overdueLoanScheduleDatas);
void recalculateInterest(long loanId);
CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command);
@@ -117,6 +105,8 @@ public interface LoanWritePlatformService {
CommandProcessingResult makeGLIMLoanRepayment(Long loanId, JsonCommand command);
void updateOriginalSchedule(Loan loan);
CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command);
CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command);
@@ -46,9 +46,7 @@ import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
@@ -315,17 +313,9 @@ public final class LoanProductDataValidator {
final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE,
element);
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notNull();
LoanRepaymentScheduleTransactionProcessor processor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(transactionProcessingStrategyCode);
if (processor == null) {
Object[] values = loanRepaymentScheduleTransactionProcessorFactory.getStrategies().stream()
.map(TransactionProcessingStrategyData::getCode).toList().toArray(new String[0]);
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).isOneOfTheseValues(values);
}
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank();
// Validating whether the processor is existing
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);
if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME, element)) {
final Long delinquencyBucketId = this.fromApiJsonHelper.extractLongNamed(LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME,
@@ -1093,8 +1083,9 @@ public final class LoanProductDataValidator {
if (this.fromApiJsonHelper.parameterExists(TRANSACTION_PROCESSING_STRATEGY_CODE, element)) {
final String transactionProcessingStrategyCode = this.fromApiJsonHelper.extractStringNamed(TRANSACTION_PROCESSING_STRATEGY_CODE,
element);
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notNull()
.integerGreaterThanZero();
baseDataValidator.reset().parameter(TRANSACTION_PROCESSING_STRATEGY_CODE).value(transactionProcessingStrategyCode).notBlank();
// Validating whether the processor is existing
loanRepaymentScheduleTransactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);
}
// grace validation
@@ -289,6 +289,8 @@ public final class LoanEnumerations {
LoanTransactionType.CHARGE_REFUND.getCode(), "Charge Refund");
case CHARGEBACK -> new LoanTransactionEnumData(LoanTransactionType.CHARGEBACK.getValue().longValue(),
LoanTransactionType.CHARGEBACK.getCode(), "Chargeback");
case CHARGE_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.CHARGE_ADJUSTMENT.getValue().longValue(),
LoanTransactionType.CHARGE_ADJUSTMENT.getCode(), "Charge Adjustment");
};
}
@@ -87,4 +87,6 @@
<include file="parts/0065_add_bypass_loan_write_transaction_permission.xml" relativeToChangelogFile="true"/>
<include file="parts/0066_delinquency_classification_chargeback.xml" relativeToChangelogFile="true"/>
<include file="parts/0067_add_configurations_for_repayment_due_business_steps.xml" relativeToChangelogFile="true"/>
<include file="parts/0068_loan_charge_adjustment.xml" relativeToChangelogFile="true"/>
<include file="parts/0069_add_unique_constraint_for_reversal_external_id_of_loan_transactions.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,58 @@
<?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_external_event_configuration">
<column name="type" value="LoanChargeAdjustmentPostBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanChargeAdjustmentPreBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="2" author="fineract">
<addColumn tableName="m_loan_transaction_relation">
<column name="to_loan_charge_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
<dropNotNullConstraint columnDataType="BIGINT"
columnName="to_loan_transaction_id"
tableName="m_loan_transaction_relation"/>
<addForeignKeyConstraint baseColumnNames="to_loan_charge_id" baseTableName="m_loan_transaction_relation"
constraintName="FK_m_loan_charge_to_relation" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="m_loan_charge" validate="true"/>
</changeSet>
<changeSet id="3" author="fineract">
<insert tableName="m_permission">
<column name="grouping" value="transaction_loan"/>
<column name="code" value="ADJUSTMENT_LOANCHARGE"/>
<column name="entity_name" value="LOANCHARGE"/>
<column name="action_name" value="ADJUSTMENT"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,29 @@
<?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">
<addUniqueConstraint columnNames="reversal_external_id," constraintName="UNIQUE_REVERSAL_EXTERNAL_ID_LOAN_TRANSACTION"
tableName="m_loan_transaction"/>
</changeSet>
</databaseChangeLog>
@@ -88,7 +88,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"RecurringDepositAccountCreateBusinessEvent", "SavingsActivateBusinessEvent", "SavingsApproveBusinessEvent",
"SavingsCloseBusinessEvent", "SavingsCreateBusinessEvent", "SavingsDepositBusinessEvent",
"SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent",
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent");
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent",
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -160,7 +161,8 @@ public class ExternalEventConfigurationValidationServiceTest {
"RecurringDepositAccountCreateBusinessEvent", "SavingsActivateBusinessEvent", "SavingsApproveBusinessEvent",
"SavingsCloseBusinessEvent", "SavingsCreateBusinessEvent", "SavingsDepositBusinessEvent",
"SavingsPostInterestBusinessEvent", "SavingsRejectBusinessEvent", "SavingsWithdrawalBusinessEvent",
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent");
"ShareAccountApproveBusinessEvent", "ShareAccountCreateBusinessEvent", "ShareProductDividentsCreateBusinessEvent",
"LoanChargeAdjustmentPostBusinessEvent", "LoanChargeAdjustmentPreBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -41,7 +41,7 @@ class DefaultLoanLifecycleStateMachineTest {
@BeforeEach
public void setUp() {
underTest = new DefaultLoanLifecycleStateMachine(LoanStatus.values(), businessEventNotifierService);
underTest = new DefaultLoanLifecycleStateMachine(businessEventNotifierService);
}
@Test