FINERACT-2081: Payments after loan charge - payment allocation
This commit is contained in:
committed by
Adam Saghy
parent
2d0c8db993
commit
31c9079ad9
+18
-14
@@ -704,8 +704,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.charges.add(loanCharge);
|
||||
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
|
||||
|
||||
// store Id's of existing loan transactions and existing reversed loan
|
||||
// transactions
|
||||
// store Id's of existing loan transactions and existing reversed loan transactions
|
||||
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
|
||||
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge);
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -721,7 +720,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
}
|
||||
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
@@ -729,6 +727,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return changedTransactionDetail;
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail reprocessTransactionsWithPostTransactionChecks(LocalDate transactionDate) {
|
||||
ChangedTransactionDetail changedDetail = reprocessTransactions();
|
||||
doPostLoanTransactionChecks(transactionDate, loanLifecycleStateMachine);
|
||||
return changedDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The
|
||||
* newly created transaction is also added to the Loan on which this method is called.
|
||||
@@ -2499,7 +2503,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
private boolean doPostLoanTransactionChecks(final LocalDate transactionDate,
|
||||
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
|
||||
boolean statusChanged = false;
|
||||
boolean isOverpaid = getTotalOverpaid() != null && getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
|
||||
boolean isOverpaid = MathUtil.isGreaterThanZero(totalOverpaid);
|
||||
if (isOverpaid) {
|
||||
// FIXME - kw - update account balance to negative amount.
|
||||
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
|
||||
@@ -2510,7 +2514,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
} else {
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
||||
}
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||
this.overpaidOnDate = null;
|
||||
}
|
||||
return statusChanged;
|
||||
@@ -2526,8 +2530,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.actualMaturityDate = transactionDate;
|
||||
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this);
|
||||
|
||||
} else if (LoanStatus.fromInt(this.loanStatus).isOverpaid()) {
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
} else if (getStatus().isOverpaid()) {
|
||||
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||
this.overpaidOnDate = null;
|
||||
}
|
||||
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
|
||||
@@ -4498,7 +4502,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
|
||||
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
|
||||
Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
|
||||
if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
|
||||
if (loanTransaction.isOverPaid()) {
|
||||
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
|
||||
if (this.getCreditAllocationRules() != null && this.getCreditAllocationRules().size() > 0) {
|
||||
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
|
||||
@@ -4792,7 +4796,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
|
||||
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
|
||||
if (MathUtil.isEmpty(totalOverpaid)) {
|
||||
this.overpaidOnDate = null;
|
||||
this.closedOnDate = newCreditBalanceRefundTransaction.getTransactionDate();
|
||||
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, this);
|
||||
@@ -5497,12 +5501,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getLoanTransactions().stream().filter(predicate).toList();
|
||||
}
|
||||
|
||||
public LoanTransaction getLoanTransaction(Predicate<LoanTransaction> predicate) {
|
||||
return getLoanTransactions().stream().filter(predicate).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public LoanTransaction findChargedOffTransaction() {
|
||||
return getLoanTransactions().stream() //
|
||||
.filter(LoanTransaction::isNotReversed) //
|
||||
.filter(LoanTransaction::isChargeOff) //
|
||||
.findFirst() //
|
||||
.orElse(null);
|
||||
return getLoanTransaction(e -> e.isNotReversed() && e.isChargeOff());
|
||||
}
|
||||
|
||||
public void handleMaturityDateActivate() {
|
||||
|
||||
+86
-23
@@ -38,9 +38,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
@@ -303,6 +305,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
public LoanTransaction copyTransactionPropertiesAndMappings() {
|
||||
LoanTransaction newTransaction = copyTransactionProperties(this);
|
||||
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
|
||||
newTransaction.updateLoanChargePaidMappings(loanChargesPaid);
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
|
||||
final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
|
||||
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
|
||||
@@ -920,37 +929,84 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
|
||||
public void updateLoanTransactionToRepaymentScheduleMappings(final Collection<LoanTransactionToRepaymentScheduleMapping> mappings) {
|
||||
Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings = new ArrayList<>();
|
||||
for (LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping : mappings) {
|
||||
updateMapingDetail(retainMappings, updatedrepaymentScheduleMapping);
|
||||
for (LoanTransactionToRepaymentScheduleMapping updatedMapping : mappings) {
|
||||
updateMappingDetail(retainMappings, updatedMapping, true);
|
||||
}
|
||||
this.loanTransactionToRepaymentScheduleMappings.retainAll(retainMappings);
|
||||
}
|
||||
|
||||
private boolean updateMapingDetail(final Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings,
|
||||
final LoanTransactionToRepaymentScheduleMapping updatedrepaymentScheduleMapping) {
|
||||
public void addLoanTransactionToRepaymentScheduleMappings(final Collection<LoanTransactionToRepaymentScheduleMapping> updatedMappings) {
|
||||
for (LoanTransactionToRepaymentScheduleMapping updatedMapping : updatedMappings) {
|
||||
updateMappingDetail(null, updatedMapping, false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean updateMappingDetail(final Collection<LoanTransactionToRepaymentScheduleMapping> retainMappings,
|
||||
final LoanTransactionToRepaymentScheduleMapping updatedMapping, boolean overwrite) {
|
||||
boolean isMappingUpdated = false;
|
||||
for (LoanTransactionToRepaymentScheduleMapping repaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) {
|
||||
if (updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId() != null
|
||||
&& repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate()
|
||||
.equals(updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getDueDate())
|
||||
&& updatedrepaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId()
|
||||
.equals(repaymentScheduleMapping.getLoanRepaymentScheduleInstallment().getId())) {
|
||||
repaymentScheduleMapping.setComponents(updatedrepaymentScheduleMapping.getPrincipalPortion(),
|
||||
updatedrepaymentScheduleMapping.getInterestPortion(), updatedrepaymentScheduleMapping.getFeeChargesPortion(),
|
||||
updatedrepaymentScheduleMapping.getPenaltyChargesPortion());
|
||||
LoanRepaymentScheduleInstallment updatedInstallment = updatedMapping.getLoanRepaymentScheduleInstallment();
|
||||
for (LoanTransactionToRepaymentScheduleMapping existingMapping : this.loanTransactionToRepaymentScheduleMappings) {
|
||||
LoanRepaymentScheduleInstallment existingInstallment = existingMapping.getLoanRepaymentScheduleInstallment();
|
||||
if (DateUtils.isEqual(existingInstallment.getDueDate(), updatedInstallment.getDueDate()) && updatedInstallment.getId() != null
|
||||
&& updatedInstallment.getId().equals(existingInstallment.getId())) {
|
||||
if (overwrite) {
|
||||
existingMapping.setComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||
} else {
|
||||
existingMapping.updateComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||
}
|
||||
isMappingUpdated = true;
|
||||
retainMappings.add(repaymentScheduleMapping);
|
||||
if (retainMappings != null) {
|
||||
retainMappings.add(existingMapping);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isMappingUpdated) {
|
||||
updatedrepaymentScheduleMapping.setLoanTransaction(this);
|
||||
this.loanTransactionToRepaymentScheduleMappings.add(updatedrepaymentScheduleMapping);
|
||||
retainMappings.add(updatedrepaymentScheduleMapping);
|
||||
LoanTransactionToRepaymentScheduleMapping newMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(this,
|
||||
updatedInstallment, null, null, null, null);
|
||||
newMapping.setComponents(updatedMapping.getPrincipalPortion(), updatedMapping.getInterestPortion(),
|
||||
updatedMapping.getFeeChargesPortion(), updatedMapping.getPenaltyChargesPortion());
|
||||
this.loanTransactionToRepaymentScheduleMappings.add(newMapping);
|
||||
if (retainMappings != null) {
|
||||
retainMappings.add(newMapping);
|
||||
}
|
||||
}
|
||||
return isMappingUpdated;
|
||||
}
|
||||
|
||||
public void updateLoanChargePaidMappings(final Collection<LoanChargePaidBy> updatedMappings) {
|
||||
Collection<LoanChargePaidBy> retainMappings = new ArrayList<>();
|
||||
for (LoanChargePaidBy updatedMapping : updatedMappings) {
|
||||
updateLoanChargePaid(retainMappings, updatedMapping);
|
||||
}
|
||||
this.loanChargesPaid.retainAll(retainMappings);
|
||||
}
|
||||
|
||||
private boolean updateLoanChargePaid(final Collection<LoanChargePaidBy> retainMappings, final LoanChargePaidBy updatedMapping) {
|
||||
boolean updated = false;
|
||||
LoanCharge updatedCharge = updatedMapping.getLoanCharge();
|
||||
Integer updatedInstallment = updatedMapping.getInstallmentNumber();
|
||||
for (LoanChargePaidBy existingMapping : loanChargesPaid) {
|
||||
LoanCharge existingCharge = existingMapping.getLoanCharge();
|
||||
Integer existingInstallment = existingMapping.getInstallmentNumber();
|
||||
if (existingCharge.equals(updatedCharge)
|
||||
&& (existingInstallment == null ? updatedInstallment == null : existingInstallment.equals(updatedInstallment))) {
|
||||
existingMapping.setAmount(updatedMapping.getAmount());
|
||||
updated = true;
|
||||
retainMappings.add(existingMapping);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
LoanChargePaidBy newMapping = new LoanChargePaidBy(this, updatedCharge, updatedMapping.getAmount(), updatedInstallment);
|
||||
this.loanChargesPaid.add(newMapping);
|
||||
retainMappings.add(newMapping);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
public Set<LoanTransactionToRepaymentScheduleMapping> getLoanTransactionToRepaymentScheduleMappings() {
|
||||
return this.loanTransactionToRepaymentScheduleMappings;
|
||||
}
|
||||
@@ -989,18 +1045,21 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
}
|
||||
|
||||
public boolean hasLoanTransactionRelations() {
|
||||
return (loanTransactionRelations != null && loanTransactionRelations.size() > 0);
|
||||
}
|
||||
|
||||
public boolean hasChargebackLoanTransactionRelations() {
|
||||
return (loanTransactionRelations != null
|
||||
&& loanTransactionRelations.stream().anyMatch(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType())));
|
||||
return !loanTransactionRelations.isEmpty();
|
||||
}
|
||||
|
||||
public Set<LoanTransactionRelation> getLoanTransactionRelations() {
|
||||
return loanTransactionRelations;
|
||||
}
|
||||
|
||||
public List<LoanTransactionRelation> getLoanTransactionRelations(Predicate<LoanTransactionRelation> predicate) {
|
||||
return loanTransactionRelations.stream().filter(predicate).toList();
|
||||
}
|
||||
|
||||
public boolean hasChargebackLoanTransactionRelations() {
|
||||
return !getLoanTransactionRelations(e -> LoanTransactionRelationTypeEnum.CHARGEBACK.equals(e.getRelationType())).isEmpty();
|
||||
}
|
||||
|
||||
public void copyLoanTransactionRelations(Set<LoanTransactionRelation> sourceLoanTransactionRelations) {
|
||||
for (LoanTransactionRelation existingLoanTransactionRelation : sourceLoanTransactionRelations) {
|
||||
loanTransactionRelations.add(new LoanTransactionRelation(this, existingLoanTransactionRelation.getToTransaction(),
|
||||
@@ -1055,6 +1114,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return chargeRefundChargeType;
|
||||
}
|
||||
|
||||
public boolean isOverPaid() {
|
||||
return MathUtil.isGreaterThanZero(overPaymentPortion);
|
||||
}
|
||||
|
||||
// TODO missing hashCode(), equals(Object obj), but probably OK as long as
|
||||
// this is never stored in a Collection.
|
||||
}
|
||||
|
||||
+16
-14
@@ -24,8 +24,10 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@@ -77,15 +79,11 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
final Money feeChargesPortion, final Money penaltyChargesPortion) {
|
||||
return new LoanTransactionToRepaymentScheduleMapping(loanTransaction, installment, defaultToNullIfZero(principalPortion),
|
||||
defaultToNullIfZero(interestPortion), defaultToNullIfZero(feeChargesPortion), defaultToNullIfZero(penaltyChargesPortion),
|
||||
defaultToNullIfZero(principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion)));
|
||||
defaultToNullIfZero(MathUtil.plus(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)));
|
||||
}
|
||||
|
||||
private static BigDecimal defaultToNullIfZero(final Money value) {
|
||||
BigDecimal result = value.getAmount();
|
||||
if (value.isZero()) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
return (value == null || value.isZero()) ? null : value.getAmount();
|
||||
}
|
||||
|
||||
private BigDecimal defaultToZeroIfNull(final BigDecimal value) {
|
||||
@@ -100,10 +98,15 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
return this.installment;
|
||||
}
|
||||
|
||||
public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
|
||||
final MonetaryCurrency currency = principal.getCurrency();
|
||||
this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal));
|
||||
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest));
|
||||
public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges,
|
||||
@NotNull Money penaltyCharges) {
|
||||
updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
|
||||
}
|
||||
|
||||
void updateComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
|
||||
final BigDecimal penaltyCharges) {
|
||||
this.principalPortion = MathUtil.zeroToNull(MathUtil.add(getPrincipalPortion(), principal));
|
||||
this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), interest));
|
||||
updateChargesComponents(feeCharges, penaltyCharges);
|
||||
updateAmount();
|
||||
}
|
||||
@@ -122,10 +125,9 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
updateAmount();
|
||||
}
|
||||
|
||||
private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) {
|
||||
final MonetaryCurrency currency = feeCharges.getCurrency();
|
||||
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges));
|
||||
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges));
|
||||
private void updateChargesComponents(final BigDecimal feeCharges, final BigDecimal penaltyCharges) {
|
||||
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), feeCharges));
|
||||
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), penaltyCharges));
|
||||
}
|
||||
|
||||
public Money getPrincipalPortion(final MonetaryCurrency currency) {
|
||||
|
||||
+11
-1
@@ -28,7 +28,9 @@ import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
@RequiredArgsConstructor
|
||||
public enum LoanScheduleProcessingType {
|
||||
|
||||
HORIZONTAL("Horizontal"), VERTICAL("Vertical");
|
||||
HORIZONTAL("Horizontal"), //
|
||||
VERTICAL("Vertical"), //
|
||||
;
|
||||
|
||||
private final String humanReadableName;
|
||||
|
||||
@@ -39,4 +41,12 @@ public enum LoanScheduleProcessingType {
|
||||
public EnumOptionData asEnumOptionData() {
|
||||
return new EnumOptionData((long) this.ordinal(), this.name(), this.humanReadableName);
|
||||
}
|
||||
|
||||
public boolean isHorizontal() {
|
||||
return this == HORIZONTAL;
|
||||
}
|
||||
|
||||
public boolean isVertical() {
|
||||
return this == VERTICAL;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -50,4 +50,8 @@ public enum PaymentAllocationTransactionType {
|
||||
public static List<EnumOptionData> getValuesAsEnumOptionDataList() {
|
||||
return Arrays.stream(values()).map(v -> new EnumOptionData((long) (v.ordinal() + 1), v.name(), v.getHumanReadableName())).toList();
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
return this == DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user