[FINERACT-1968] charge payment adv.paym.alloc.strategy
This commit is contained in:
+7
-2
@@ -275,10 +275,15 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static LoanTransaction copyTransactionProperties(final LoanTransaction loanTransaction) {
|
public static LoanTransaction copyTransactionProperties(final LoanTransaction loanTransaction) {
|
||||||
return new LoanTransaction(loanTransaction.loan, loanTransaction.office, loanTransaction.typeOf, loanTransaction.dateOf,
|
LoanTransaction newTransaction = new LoanTransaction(loanTransaction.loan, loanTransaction.office, loanTransaction.typeOf,
|
||||||
loanTransaction.amount, loanTransaction.principalPortion, loanTransaction.interestPortion,
|
loanTransaction.dateOf, loanTransaction.amount, loanTransaction.principalPortion, loanTransaction.interestPortion,
|
||||||
loanTransaction.feeChargesPortion, loanTransaction.penaltyChargesPortion, loanTransaction.overPaymentPortion,
|
loanTransaction.feeChargesPortion, loanTransaction.penaltyChargesPortion, loanTransaction.overPaymentPortion,
|
||||||
loanTransaction.reversed, loanTransaction.paymentDetail, loanTransaction.externalId);
|
loanTransaction.reversed, loanTransaction.paymentDetail, loanTransaction.externalId);
|
||||||
|
|
||||||
|
if (LoanTransactionType.CHARGE_PAYMENT.equals(loanTransaction.getTypeOf())) {
|
||||||
|
newTransaction.getLoanChargesPaid().addAll(loanTransaction.getLoanChargesPaid());
|
||||||
|
}
|
||||||
|
return newTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
|
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
|
||||||
|
|||||||
+2
-1
@@ -37,7 +37,8 @@ public enum PaymentAllocationTransactionType {
|
|||||||
GOODWILL_CREDIT(LoanTransactionType.GOODWILL_CREDIT, "Goodwill credit"), //
|
GOODWILL_CREDIT(LoanTransactionType.GOODWILL_CREDIT, "Goodwill credit"), //
|
||||||
CHARGE_REFUND(LoanTransactionType.CHARGE_REFUND, "Charge refund"), //
|
CHARGE_REFUND(LoanTransactionType.CHARGE_REFUND, "Charge refund"), //
|
||||||
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT, "Charge adjustment"), //
|
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT, "Charge adjustment"), //
|
||||||
WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest");//
|
WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"), CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT,
|
||||||
|
"Charge payment");//
|
||||||
|
|
||||||
private final LoanTransactionType loanTransactionType;
|
private final LoanTransactionType loanTransactionType;
|
||||||
private final String humanReadableName;
|
private final String humanReadableName;
|
||||||
|
|||||||
+177
-98
@@ -26,6 +26,7 @@ import java.util.Collections;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@@ -38,6 +39,7 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
|||||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
@@ -183,11 +185,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
|
||||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
|
||||||
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
|
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
|
||||||
|
|
||||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||||
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
|
||||||
handleRepayment(loanTransaction, currency, installments, charges);
|
handleRepayment(loanTransaction, currency, installments, charges);
|
||||||
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
|
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments);
|
||||||
|
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges);
|
||||||
// TODO: Cover rest of the transaction types
|
// TODO: Cover rest of the transaction types
|
||||||
default -> {
|
default -> {
|
||||||
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||||
@@ -249,6 +251,153 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
loanTransaction.resetDerivedComponents();
|
loanTransaction.resetDerivedComponents();
|
||||||
}
|
}
|
||||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||||
|
processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
|
||||||
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanTransaction loanTransaction,
|
||||||
|
LoanRepaymentScheduleInstallment currentInstallment, MonetaryCurrency currency) {
|
||||||
|
Money zero = Money.zero(currency);
|
||||||
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = transactionMappings.stream()
|
||||||
|
.filter(e -> loanTransaction.equals(e.getLoanTransaction()))
|
||||||
|
.filter(e -> currentInstallment.equals(e.getLoanRepaymentScheduleInstallment())).findFirst().orElse(null);
|
||||||
|
if (loanTransactionToRepaymentScheduleMapping == null) {
|
||||||
|
loanTransactionToRepaymentScheduleMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
|
||||||
|
currentInstallment, zero, zero, zero, zero);
|
||||||
|
transactionMappings.add(loanTransactionToRepaymentScheduleMapping);
|
||||||
|
}
|
||||||
|
return loanTransactionToRepaymentScheduleMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Money payAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
|
||||||
|
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||||
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) {
|
||||||
|
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
|
Money zero = transactionAmountUnprocessed.zero();
|
||||||
|
return switch (paymentAllocationType.getAllocationType()) {
|
||||||
|
case PENALTY -> {
|
||||||
|
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed);
|
||||||
|
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(subPenaltyPortion));
|
||||||
|
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, subPenaltyPortion);
|
||||||
|
yield subPenaltyPortion;
|
||||||
|
}
|
||||||
|
case FEE -> {
|
||||||
|
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountUnprocessed);
|
||||||
|
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(subFeePortion));
|
||||||
|
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, subFeePortion, zero);
|
||||||
|
yield subFeePortion;
|
||||||
|
}
|
||||||
|
case INTEREST -> {
|
||||||
|
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountUnprocessed);
|
||||||
|
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(subInterestPortion));
|
||||||
|
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, subInterestPortion, zero, zero);
|
||||||
|
yield subInterestPortion;
|
||||||
|
}
|
||||||
|
case PRINCIPAL -> {
|
||||||
|
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountUnprocessed);
|
||||||
|
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(subPrincipalPortion));
|
||||||
|
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, subPrincipalPortion, zero, zero, zero);
|
||||||
|
yield subPrincipalPortion;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping,
|
||||||
|
Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) {
|
||||||
|
BigDecimal aggregatedPenalty = ObjectUtils
|
||||||
|
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPenaltyChargesPortion(), BigDecimal.ZERO)
|
||||||
|
.add(penaltyPortion.getAmount());
|
||||||
|
BigDecimal aggregatedFee = ObjectUtils
|
||||||
|
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getFeeChargesPortion(), BigDecimal.ZERO)
|
||||||
|
.add(feePortion.getAmount());
|
||||||
|
BigDecimal aggregatedInterest = ObjectUtils
|
||||||
|
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getInterestPortion(), BigDecimal.ZERO)
|
||||||
|
.add(interestPortion.getAmount());
|
||||||
|
BigDecimal aggregatedPrincipal = ObjectUtils
|
||||||
|
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPrincipalPortion(), BigDecimal.ZERO)
|
||||||
|
.add(principalPortion.getAmount());
|
||||||
|
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction) {
|
||||||
|
if (overpaymentPortion.isGreaterThanZero()) {
|
||||||
|
onLoanOverpayment(loanTransaction, overpaymentPortion);
|
||||||
|
loanTransaction.updateOverPayments(overpaymentPortion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleChargeOff(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
|
loanTransaction.resetDerivedComponents();
|
||||||
|
// determine how much is outstanding total and breakdown for principal, interest and charges
|
||||||
|
Money principalPortion = Money.zero(currency);
|
||||||
|
Money interestPortion = Money.zero(currency);
|
||||||
|
Money feeChargesPortion = Money.zero(currency);
|
||||||
|
Money penaltychargesPortion = Money.zero(currency);
|
||||||
|
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
||||||
|
if (currentInstallment.isNotFullyPaidOff()) {
|
||||||
|
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
|
||||||
|
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
|
||||||
|
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency));
|
||||||
|
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||||
|
Money zero = Money.zero(currency);
|
||||||
|
Money feeChargesPortion = zero;
|
||||||
|
Money penaltyChargesPortion = zero;
|
||||||
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
|
LoanChargePaidBy loanChargePaidBy = loanTransaction.getLoanChargesPaid().stream().findFirst().get();
|
||||||
|
LoanCharge loanCharge = loanChargePaidBy.getLoanCharge();
|
||||||
|
Money amountToBePaid = Money.of(currency, loanTransaction.getAmount());
|
||||||
|
if (loanCharge.getAmountOutstanding(currency).isLessThan(amountToBePaid)) {
|
||||||
|
amountToBePaid = loanCharge.getAmountOutstanding(currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate startDate = loanTransaction.getLoan().getDisbursementDate();
|
||||||
|
|
||||||
|
Money unprocessed = loanTransaction.getAmount(currency);
|
||||||
|
for (final LoanRepaymentScheduleInstallment installment : installments) {
|
||||||
|
boolean isDue = installment.isFirstPeriod()
|
||||||
|
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(startDate, installment.getDueDate())
|
||||||
|
: loanCharge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate());
|
||||||
|
if (isDue) {
|
||||||
|
Integer installmentNumber = installment.getInstallmentNumber();
|
||||||
|
Money paidAmount = loanCharge.updatePaidAmountBy(amountToBePaid, installmentNumber, zero);
|
||||||
|
|
||||||
|
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
|
||||||
|
transactionMappings, loanTransaction, installment, currency);
|
||||||
|
|
||||||
|
if (loanTransaction.isPenaltyPayment()) {
|
||||||
|
penaltyChargesPortion = installment.payPenaltyChargesComponent(loanTransaction.getTransactionDate(), paidAmount);
|
||||||
|
loanTransaction.setLoanChargesPaid(Collections
|
||||||
|
.singleton(new LoanChargePaidBy(loanTransaction, loanCharge, paidAmount.getAmount(), installmentNumber)));
|
||||||
|
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, penaltyChargesPortion);
|
||||||
|
} else {
|
||||||
|
feeChargesPortion = installment.payFeeChargesComponent(loanTransaction.getTransactionDate(), paidAmount);
|
||||||
|
loanTransaction.setLoanChargesPaid(Collections
|
||||||
|
.singleton(new LoanChargePaidBy(loanTransaction, loanCharge, paidAmount.getAmount(), installmentNumber)));
|
||||||
|
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, feeChargesPortion, zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
loanTransaction.updateComponents(zero, zero, feeChargesPortion, penaltyChargesPortion);
|
||||||
|
unprocessed = loanTransaction.getAmount(currency).minus(paidAmount);
|
||||||
|
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unprocessed.isGreaterThanZero()) {
|
||||||
|
processTransaction(loanTransaction, currency, installments, unprocessed, charges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, Set<LoanCharge> charges) {
|
||||||
Money zero = Money.zero(currency);
|
Money zero = Money.zero(currency);
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||||
|
|
||||||
@@ -345,116 +494,46 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||||
|
|
||||||
|
payAdditionalCharges(loanTransaction, currency, charges);
|
||||||
|
|
||||||
|
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void payAdditionalCharges(LoanTransaction loanTransaction, MonetaryCurrency currency, Set<LoanCharge> charges) {
|
||||||
|
final Set<LoanCharge> paidFeeCharges = loanTransaction.getLoanChargesPaid().stream() //
|
||||||
|
.map(LoanChargePaidBy::getLoanCharge) //
|
||||||
|
.filter(LoanCharge::isFeeCharge).collect(Collectors.toSet());
|
||||||
|
final Set<LoanCharge> paidPenaltyCharges = loanTransaction.getLoanChargesPaid().stream() //
|
||||||
|
.map(LoanChargePaidBy::getLoanCharge) //
|
||||||
|
.filter(LoanCharge::isPenaltyCharge).collect(Collectors.toSet());
|
||||||
// TODO: rewrite to provide sorted list
|
// TODO: rewrite to provide sorted list
|
||||||
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
|
||||||
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
|
||||||
|
loanFees.removeAll(paidFeeCharges);
|
||||||
|
loanPenalties.removeAll(paidPenaltyCharges);
|
||||||
|
|
||||||
|
BigDecimal sumFeePaidAmount = paidFeeCharges.stream() //
|
||||||
|
.map(paidCharge -> paidCharge.getAmountPaid(currency)) //
|
||||||
|
.map(Money::getAmount) //
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
BigDecimal sumPenaltyPaidAmount = paidPenaltyCharges.stream() //
|
||||||
|
.map(paidCharge -> paidCharge.getAmountPaid(currency)) //
|
||||||
|
.map(Money::getAmount) //
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) {
|
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) {
|
||||||
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
|
Money feeCharges = loanTransaction.getFeeChargesPortion(currency).minus(sumFeePaidAmount);
|
||||||
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
|
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency).minus(sumPenaltyPaidAmount);
|
||||||
|
|
||||||
if (feeCharges.isGreaterThanZero()) {
|
if (feeCharges.isGreaterThanZero()) {
|
||||||
// TODO: Rewrite to exclude charge payment
|
|
||||||
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, null);
|
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (penaltyCharges.isGreaterThanZero()) {
|
if (penaltyCharges.isGreaterThanZero()) {
|
||||||
// TODO: Rewrite to exclude charge payment
|
|
||||||
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, null);
|
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
|
|
||||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanTransaction loanTransaction,
|
|
||||||
LoanRepaymentScheduleInstallment currentInstallment, MonetaryCurrency currency) {
|
|
||||||
Money zero = Money.zero(currency);
|
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = transactionMappings.stream()
|
|
||||||
.filter(e -> loanTransaction.equals(e.getLoanTransaction()))
|
|
||||||
.filter(e -> currentInstallment.equals(e.getLoanRepaymentScheduleInstallment())).findFirst().orElse(null);
|
|
||||||
if (loanTransactionToRepaymentScheduleMapping == null) {
|
|
||||||
loanTransactionToRepaymentScheduleMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
|
|
||||||
currentInstallment, zero, zero, zero, zero);
|
|
||||||
transactionMappings.add(loanTransactionToRepaymentScheduleMapping);
|
|
||||||
}
|
|
||||||
return loanTransactionToRepaymentScheduleMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Money payAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
|
|
||||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
|
||||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) {
|
|
||||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
|
||||||
Money zero = transactionAmountUnprocessed.zero();
|
|
||||||
return switch (paymentAllocationType.getAllocationType()) {
|
|
||||||
case PENALTY -> {
|
|
||||||
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed);
|
|
||||||
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(subPenaltyPortion));
|
|
||||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, subPenaltyPortion);
|
|
||||||
yield subPenaltyPortion;
|
|
||||||
}
|
|
||||||
case FEE -> {
|
|
||||||
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountUnprocessed);
|
|
||||||
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(subFeePortion));
|
|
||||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, subFeePortion, zero);
|
|
||||||
yield subFeePortion;
|
|
||||||
}
|
|
||||||
case INTEREST -> {
|
|
||||||
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountUnprocessed);
|
|
||||||
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(subInterestPortion));
|
|
||||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, subInterestPortion, zero, zero);
|
|
||||||
yield subInterestPortion;
|
|
||||||
}
|
|
||||||
case PRINCIPAL -> {
|
|
||||||
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountUnprocessed);
|
|
||||||
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(subPrincipalPortion));
|
|
||||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, subPrincipalPortion, zero, zero, zero);
|
|
||||||
yield subPrincipalPortion;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping,
|
|
||||||
Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) {
|
|
||||||
BigDecimal aggregatedPenalty = ObjectUtils
|
|
||||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPenaltyChargesPortion(), BigDecimal.ZERO)
|
|
||||||
.add(penaltyPortion.getAmount());
|
|
||||||
BigDecimal aggregatedFee = ObjectUtils
|
|
||||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getFeeChargesPortion(), BigDecimal.ZERO)
|
|
||||||
.add(feePortion.getAmount());
|
|
||||||
BigDecimal aggregatedInterest = ObjectUtils
|
|
||||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getInterestPortion(), BigDecimal.ZERO)
|
|
||||||
.add(interestPortion.getAmount());
|
|
||||||
BigDecimal aggregatedPrincipal = ObjectUtils
|
|
||||||
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPrincipalPortion(), BigDecimal.ZERO)
|
|
||||||
.add(principalPortion.getAmount());
|
|
||||||
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction) {
|
|
||||||
if (overpaymentPortion.isGreaterThanZero()) {
|
|
||||||
onLoanOverpayment(loanTransaction, overpaymentPortion);
|
|
||||||
loanTransaction.updateOverPayments(overpaymentPortion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleChargeOff(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
|
||||||
List<LoanRepaymentScheduleInstallment> installments) {
|
|
||||||
loanTransaction.resetDerivedComponents();
|
|
||||||
// determine how much is outstanding total and breakdown for principal, interest and charges
|
|
||||||
Money principalPortion = Money.zero(currency);
|
|
||||||
Money interestPortion = Money.zero(currency);
|
|
||||||
Money feeChargesPortion = Money.zero(currency);
|
|
||||||
Money penaltychargesPortion = Money.zero(currency);
|
|
||||||
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
|
|
||||||
if (currentInstallment.isNotFullyPaidOff()) {
|
|
||||||
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
|
|
||||||
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
|
|
||||||
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency));
|
|
||||||
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
|||||||
+360
@@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.integrationtests;
|
||||||
|
|
||||||
|
import static org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity.LIABILITY_TRANSFER;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import io.restassured.builder.RequestSpecBuilder;
|
||||||
|
import io.restassured.builder.ResponseSpecBuilder;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.specification.RequestSpecification;
|
||||||
|
import io.restassured.specification.ResponseSpecification;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeFormatterBuilder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||||
|
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.PaymentAllocationOrder;
|
||||||
|
import org.apache.fineract.client.models.PostClientsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
|
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||||
|
import org.apache.fineract.integrationtests.common.BusinessDateHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.CommonConstants;
|
||||||
|
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
|
import org.apache.fineract.integrationtests.common.accounting.Account;
|
||||||
|
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.accounting.FinancialActivityAccountHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
@ExtendWith(LoanTestLifecycleExtension.class)
|
||||||
|
@Slf4j
|
||||||
|
public class LoanChargePaymentWithAdvancedPaymentAllocationTest {
|
||||||
|
|
||||||
|
private static final String DATETIME_PATTERN = "dd MMMM yyyy";
|
||||||
|
private static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder().appendPattern(DATETIME_PATTERN).toFormatter();
|
||||||
|
private static RequestSpecification requestSpec;
|
||||||
|
private static ResponseSpecification responseSpec;
|
||||||
|
private static LoanTransactionHelper loanTransactionHelper;
|
||||||
|
private static AccountHelper accountHelper;
|
||||||
|
private static Integer commonLoanProductId;
|
||||||
|
private static PostClientsResponse client;
|
||||||
|
private static BusinessDateHelper businessDateHelper;
|
||||||
|
private static SchedulerJobHelper scheduleJobHelper;
|
||||||
|
private static SavingsAccountHelper savingsAccountHelper;
|
||||||
|
private static SavingsProductHelper savingsProductHelper;
|
||||||
|
private static FinancialActivityAccountHelper financialActivityAccountHelper;
|
||||||
|
private static Integer financialActivityAccountId;
|
||||||
|
private static Account liabilityTransferAccount;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setup() {
|
||||||
|
Utils.initializeRESTAssured();
|
||||||
|
ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
|
||||||
|
requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||||
|
requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||||
|
requestSpec.header("Fineract-Platform-TenantId", "default");
|
||||||
|
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||||
|
|
||||||
|
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
|
||||||
|
accountHelper = new AccountHelper(requestSpec, responseSpec);
|
||||||
|
final Account assetAccount = accountHelper.createAssetAccount();
|
||||||
|
final Account incomeAccount = accountHelper.createIncomeAccount();
|
||||||
|
final Account expenseAccount = accountHelper.createExpenseAccount();
|
||||||
|
final Account overpaymentAccount = accountHelper.createLiabilityAccount();
|
||||||
|
client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||||
|
businessDateHelper = new BusinessDateHelper();
|
||||||
|
scheduleJobHelper = new SchedulerJobHelper(requestSpec);
|
||||||
|
savingsAccountHelper = new SavingsAccountHelper(requestSpec, responseSpec);
|
||||||
|
savingsProductHelper = new SavingsProductHelper();
|
||||||
|
commonLoanProductId = createLoanProduct("500", "15", "4", assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
|
||||||
|
financialActivityAccountHelper = new FinancialActivityAccountHelper(requestSpec);
|
||||||
|
|
||||||
|
List<HashMap> financialActivities = financialActivityAccountHelper.getAllFinancialActivityAccounts(responseSpec);
|
||||||
|
if (financialActivities.isEmpty()) {
|
||||||
|
/** Setup liability transfer account **/
|
||||||
|
/** Create a Liability and an Asset Transfer Account **/
|
||||||
|
liabilityTransferAccount = accountHelper.createLiabilityAccount();
|
||||||
|
Assertions.assertNotNull(liabilityTransferAccount);
|
||||||
|
|
||||||
|
/*** Create A Financial Activity to Account Mapping **/
|
||||||
|
financialActivityAccountId = (Integer) financialActivityAccountHelper.createFinancialActivityAccount(
|
||||||
|
LIABILITY_TRANSFER.getValue(), liabilityTransferAccount.getAccountID(), responseSpec,
|
||||||
|
CommonConstants.RESPONSE_RESOURCE_ID);
|
||||||
|
Assertions.assertNotNull(financialActivityAccountId);
|
||||||
|
} else {
|
||||||
|
for (HashMap financialActivity : financialActivities) {
|
||||||
|
HashMap financialActivityData = (HashMap) financialActivity.get("financialActivityData");
|
||||||
|
if (financialActivityData.get("id").equals(FinancialActivityAccountsTest.LIABILITY_TRANSFER_FINANCIAL_ACTIVITY_ID)) {
|
||||||
|
HashMap glAccountData = (HashMap) financialActivity.get("glAccountData");
|
||||||
|
liabilityTransferAccount = new Account((Integer) glAccountData.get("id"), Account.AccountType.LIABILITY);
|
||||||
|
financialActivityAccountId = (Integer) financialActivity.get("id");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void tearDown() {
|
||||||
|
Integer deletedFinancialActivityAccountId = financialActivityAccountHelper
|
||||||
|
.deleteFinancialActivityAccount(financialActivityAccountId, responseSpec, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||||
|
Assertions.assertNotNull(deletedFinancialActivityAccountId);
|
||||||
|
Assertions.assertEquals(financialActivityAccountId, deletedFinancialActivityAccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void feeAndPenaltyChargePaymentWithDefaultAllocationRuleTest() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
final String jobName = "Transfer Fee For Loans From Savings";
|
||||||
|
final String startDate = "10 April 2022";
|
||||||
|
|
||||||
|
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
|
||||||
|
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
|
||||||
|
.date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en"));
|
||||||
|
|
||||||
|
final Integer savingsId = createSavingsAccountDailyPosting(client.getClientId().intValue(), startDate);
|
||||||
|
|
||||||
|
savingsAccountHelper.depositToSavingsAccount(savingsId, "10000", startDate, CommonConstants.RESPONSE_RESOURCE_ID);
|
||||||
|
|
||||||
|
final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 1000L, 45, 15, 3, 0,
|
||||||
|
"01 January 2023", "01 January 2023");
|
||||||
|
|
||||||
|
int loanId = loanResponse.getLoanId().intValue();
|
||||||
|
|
||||||
|
loanTransactionHelper.updateLoan(loanId,
|
||||||
|
updateLoanJson(client.getClientId().intValue(), commonLoanProductId, savingsId.toString()));
|
||||||
|
|
||||||
|
loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
|
||||||
|
.approvedOnDate("01 January 2023").locale("en"));
|
||||||
|
|
||||||
|
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
|
||||||
|
new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionAmount(BigDecimal.valueOf(1000.00)).locale("en"));
|
||||||
|
|
||||||
|
final float feePortion = 50.0f;
|
||||||
|
final float penaltyPortion = 100.0f;
|
||||||
|
|
||||||
|
Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||||
|
ChargesHelper.getLoanSpecifiedDueDateWithAccountTransferJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
|
||||||
|
String.valueOf(feePortion), false));
|
||||||
|
|
||||||
|
Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||||
|
ChargesHelper.getLoanSpecifiedDueDateWithAccountTransferJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
|
||||||
|
String.valueOf(penaltyPortion), true));
|
||||||
|
|
||||||
|
LocalDate targetDate = LocalDate.of(2023, 1, 3);
|
||||||
|
final String penaltyChargeAddedDate = DATE_FORMATTER.format(targetDate);
|
||||||
|
loanTransactionHelper.addChargesForLoan(loanId, LoanTransactionHelper
|
||||||
|
.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), penaltyChargeAddedDate, String.valueOf(feePortion)));
|
||||||
|
|
||||||
|
loanTransactionHelper.addChargesForLoan(loanId, LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(
|
||||||
|
String.valueOf(penalty), penaltyChargeAddedDate, String.valueOf(penaltyPortion)));
|
||||||
|
|
||||||
|
loanTransactionHelper.noAccrualTransactionForRepayment(loanId);
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||||
|
|
||||||
|
assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size());
|
||||||
|
assertEquals(feePortion, loanDetails.getRepaymentSchedule().getPeriods().get(2).getFeeChargesDue());
|
||||||
|
assertEquals(feePortion, loanDetails.getRepaymentSchedule().getPeriods().get(2).getFeeChargesOutstanding());
|
||||||
|
assertEquals(penaltyPortion, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesDue());
|
||||||
|
assertEquals(penaltyPortion, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesOutstanding());
|
||||||
|
assertEquals(400.0f, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalDueForPeriod());
|
||||||
|
assertEquals(400.0f, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalOutstandingForPeriod());
|
||||||
|
assertEquals(LocalDate.of(2023, 1, 16), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
|
||||||
|
|
||||||
|
scheduleJobHelper.executeAndAwaitJob(jobName);
|
||||||
|
|
||||||
|
loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
|
||||||
|
assertEquals(5, loanDetails.getRepaymentSchedule().getPeriods().size());
|
||||||
|
assertEquals(feePortion, loanDetails.getRepaymentSchedule().getPeriods().get(2).getFeeChargesDue());
|
||||||
|
assertEquals(0, loanDetails.getRepaymentSchedule().getPeriods().get(2).getFeeChargesOutstanding());
|
||||||
|
assertEquals(penaltyPortion, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesDue());
|
||||||
|
assertEquals(0, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPenaltyChargesOutstanding());
|
||||||
|
assertEquals(400, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalDueForPeriod());
|
||||||
|
assertEquals(250, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalOutstandingForPeriod());
|
||||||
|
assertEquals(LocalDate.of(2023, 1, 16), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate());
|
||||||
|
} finally {
|
||||||
|
GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer createSavingsAccountDailyPosting(final Integer clientID, String startDate) {
|
||||||
|
final Integer savingsProductID = createSavingsProductDailyPosting();
|
||||||
|
Assertions.assertNotNull(savingsProductID);
|
||||||
|
final Integer savingsId = savingsAccountHelper.applyForSavingsApplicationOnDate(clientID, savingsProductID, ACCOUNT_TYPE_INDIVIDUAL,
|
||||||
|
startDate);
|
||||||
|
Assertions.assertNotNull(savingsId);
|
||||||
|
HashMap savingsStatusHashMap = savingsAccountHelper.approveSavingsOnDate(savingsId, startDate);
|
||||||
|
SavingsStatusChecker.verifySavingsIsApproved(savingsStatusHashMap);
|
||||||
|
savingsStatusHashMap = savingsAccountHelper.activateSavingsAccount(savingsId, startDate);
|
||||||
|
SavingsStatusChecker.verifySavingsIsActive(savingsStatusHashMap);
|
||||||
|
return savingsId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer createSavingsProductDailyPosting() {
|
||||||
|
final String savingsProductJSON = savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily()
|
||||||
|
.withInterestPostingPeriodTypeAsMonthly().withInterestCalculationPeriodTypeAsDailyBalance()
|
||||||
|
.withMinimumOpenningBalance("10000.0").build();
|
||||||
|
return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments,
|
||||||
|
final Account... accounts) {
|
||||||
|
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
|
||||||
|
AdvancedPaymentData goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT");
|
||||||
|
AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION");
|
||||||
|
AdvancedPaymentData payoutRefundAllocation = createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT");
|
||||||
|
log.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
|
||||||
|
final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal)
|
||||||
|
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
|
||||||
|
.withEnableDownPayment(true, "25", true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths()
|
||||||
|
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||||
|
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
|
||||||
|
.addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation,
|
||||||
|
payoutRefundAllocation)
|
||||||
|
.withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0").build(null);
|
||||||
|
return loanTransactionHelper.getLoanProductId(loanProductJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final Long principal,
|
||||||
|
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final int interestRate,
|
||||||
|
final String expectedDisbursementDate, final String submittedOnDate) {
|
||||||
|
log.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
|
||||||
|
return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
|
||||||
|
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
|
||||||
|
.transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||||
|
.locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(interestRate)
|
||||||
|
.interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery)
|
||||||
|
.repaymentFrequencyType(0).numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency)
|
||||||
|
.loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String updateLoanJson(final Integer clientID, final Integer loanProductID, String savingsId) {
|
||||||
|
log.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
|
||||||
|
List<HashMap> collaterals = new ArrayList<>();
|
||||||
|
final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec);
|
||||||
|
Assertions.assertNotNull(collateralId);
|
||||||
|
final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec,
|
||||||
|
clientID.toString(), collateralId);
|
||||||
|
Assertions.assertNotNull(clientCollateralId);
|
||||||
|
addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1));
|
||||||
|
|
||||||
|
final String loanApplicationJSON = new LoanApplicationTestBuilder() //
|
||||||
|
.withPrincipal("1,000.00") //
|
||||||
|
.withLoanTermFrequency("45") //
|
||||||
|
.withLoanTermFrequencyAsDays() //
|
||||||
|
.withNumberOfRepayments("3") //
|
||||||
|
.withRepaymentEveryAfter("15") //
|
||||||
|
.withRepaymentFrequencyTypeAsDays() //
|
||||||
|
.withInterestRatePerPeriod("0") //
|
||||||
|
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||||
|
.withAmortizationTypeAsEqualInstallments() //
|
||||||
|
.withInterestTypeAsDecliningBalance() //
|
||||||
|
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
|
||||||
|
.withExpectedDisbursementDate("01 January 2023") //
|
||||||
|
.withSubmittedOnDate("01 January 2023") //
|
||||||
|
.withCollaterals(collaterals) //
|
||||||
|
.build(clientID.toString(), loanProductID.toString(), savingsId);
|
||||||
|
return loanApplicationJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCollaterals(List<HashMap> collaterals, Integer collateralId, BigDecimal quantity) {
|
||||||
|
collaterals.add(collaterals(collateralId, quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashMap<String, String> collaterals(Integer collateralId, BigDecimal quantity) {
|
||||||
|
HashMap<String, String> collateral = new HashMap<>(2);
|
||||||
|
collateral.put("clientCollateralId", collateralId.toString());
|
||||||
|
collateral.put("quantity", quantity.toString());
|
||||||
|
return collateral;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule) {
|
||||||
|
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
|
||||||
|
advancedPaymentData.setTransactionType(transactionType);
|
||||||
|
advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule);
|
||||||
|
|
||||||
|
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
|
||||||
|
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
|
||||||
|
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
|
||||||
|
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
|
||||||
|
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
|
||||||
|
|
||||||
|
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
|
||||||
|
return advancedPaymentData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AdvancedPaymentData createDefaultPaymentAllocation() {
|
||||||
|
AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
|
||||||
|
advancedPaymentData.setTransactionType("DEFAULT");
|
||||||
|
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
|
||||||
|
|
||||||
|
List<PaymentAllocationOrder> paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
|
||||||
|
PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST,
|
||||||
|
PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
|
||||||
|
PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
|
||||||
|
PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST);
|
||||||
|
|
||||||
|
advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
|
||||||
|
return advancedPaymentData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PaymentAllocationOrder> getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
|
||||||
|
AtomicInteger integer = new AtomicInteger(1);
|
||||||
|
return Arrays.stream(paymentAllocationTypes).map(pat -> {
|
||||||
|
PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder();
|
||||||
|
paymentAllocationOrder.setPaymentAllocationRule(pat.name());
|
||||||
|
paymentAllocationOrder.setOrder(integer.getAndIncrement());
|
||||||
|
return paymentAllocationOrder;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user