FINERACT-2286: Make configurable the outstanding interest strategy during asset owner transfer

This commit is contained in:
Adam Saghy
2025-05-22 13:27:36 +02:00
parent a3a5aabb7f
commit 18a55874ba
28 changed files with 517 additions and 59 deletions
@@ -76,6 +76,7 @@ public final class GlobalConfigurationConstants {
public static final String NEXT_PAYMENT_DUE_DATE = "next-payment-due-date";
public static final String ENABLE_PAYMENT_HUB_INTEGRATION = "enable-payment-hub-integration";
public static final String ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY = "enable-immediate-charge-accrual-post-maturity";
public static final String ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY = "outstanding-interest-calculation-strategy-for-external-asset-transfer";
private GlobalConfigurationConstants() {}
}
@@ -145,4 +145,5 @@ public interface ConfigurationDomainService {
boolean isImmediateChargeAccrualPostMaturityEnabled();
String getAssetOwnerTransferOustandingInterestStrategy();
}
@@ -74,4 +74,10 @@ public class GlobalConfigurationHelper {
boolean isEnabled = BooleanUtils.toBoolean(updatedConfiguration.body().getEnabled());
assertThat(isEnabled).isEqualTo(true);
}
public GlobalConfigurationPropertyData getGlobalConfiguration(String configKey) throws IOException {
Response<GlobalConfigurationPropertyData> configuration = globalConfigurationApi.retrieveOneByName(configKey).execute();
ErrorHelper.checkSuccessfulApiCall(configuration);
return configuration.body();
}
}
@@ -41,6 +41,7 @@ import org.apache.fineract.client.models.GetClientsClientIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdDelinquencyPausePeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GlobalConfigurationPropertyData;
import org.apache.fineract.client.models.PageExternalTransferData;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
@@ -53,6 +54,7 @@ import org.apache.fineract.test.data.AssetExternalizationTransferStatus;
import org.apache.fineract.test.data.AssetExternalizationTransferStatusReason;
import org.apache.fineract.test.data.TransactionType;
import org.apache.fineract.test.helper.ErrorMessageHelper;
import org.apache.fineract.test.helper.GlobalConfigurationHelper;
import org.apache.fineract.test.messaging.EventAssertion;
import org.apache.fineract.test.messaging.event.assetexternalization.LoanAccountSnapshotEvent;
import org.apache.fineract.test.messaging.event.assetexternalization.LoanOwnershipTransferEvent;
@@ -90,15 +92,14 @@ public class EventCheckHelper {
@Autowired
private ClientApi clientApi;
@Autowired
private LoansApi loansApi;
@Autowired
private EventAssertion eventAssertion;
@Autowired
private ExternalAssetOwnersApi externalAssetOwnersApi;
@Autowired
private GlobalConfigurationHelper configurationHelper;
public void clientEventCheck(Response<PostClientsResponse> clientCreationResponse) throws IOException {
Response<GetClientsClientIdResponse> clientDetails = clientApi.retrieveOne11(clientCreationResponse.body().getClientId(), false)
@@ -412,14 +413,24 @@ public class EventCheckHelper {
ExternalTransferData filtered = content.stream().filter(t -> transferId.equals(t.getTransferId())).reduce((first, second) -> second)
.orElseThrow(() -> new IllegalStateException("No element found"));
BigDecimal totalOutstandingBalanceAmountExpected = zeroConversion(filtered.getDetails().getTotalOutstanding());
BigDecimal outstandingInterestPortionExpected = zeroConversion(filtered.getDetails().getTotalInterestOutstanding());
GlobalConfigurationPropertyData outstandingInterestStrategy = configurationHelper
.getGlobalConfiguration("outstanding-interest-calculation-strategy-for-external-asset-transfer");
if ("PAYABLE_OUTSTANDING_INTEREST".equals(outstandingInterestStrategy.getStringValue())) {
Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "all", null, null).execute();
totalOutstandingBalanceAmountExpected = BigDecimal.valueOf(loanDetails.body().getSummary().getTotalOutstanding());
outstandingInterestPortionExpected = BigDecimal.valueOf(loanDetails.body().getSummary().getInterestOutstanding());
}
String ownerExternalIdExpected = filtered.getStatus().getValue().equals("BUYBACK") ? null : filtered.getOwner().getExternalId();
String settlementDateExpected = filtered.getStatus().getValue().equals("BUYBACK") ? null
: FORMATTER_EVENTS.format(filtered.getSettlementDate());
BigDecimal totalOutstandingBalanceAmountExpected = zeroConversion(filtered.getDetails().getTotalOutstanding());
BigDecimal outstandingPrincipalPortionExpected = zeroConversion(filtered.getDetails().getTotalPrincipalOutstanding());
BigDecimal outstandingFeePortionExpected = zeroConversion(filtered.getDetails().getTotalFeeChargesOutstanding());
BigDecimal outstandingPenaltyPortionExpected = zeroConversion(filtered.getDetails().getTotalPenaltyChargesOutstanding());
BigDecimal outstandingInterestPortionExpected = zeroConversion(filtered.getDetails().getTotalInterestOutstanding());
BigDecimal overPaymentPortionExpected = zeroConversion(filtered.getDetails().getTotalOverpaid());
eventAssertion.assertEvent(LoanAccountSnapshotEvent.class, loanId).extractingData(LoanAccountDataV1::getId).isEqualTo(loanId)
@@ -341,11 +341,6 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
transferExternalId = testContext().get(TestContextKey.ASSET_EXTERNALIZATION_SALES_TRANSFER_EXTERNAL_ID_FROM_RESPONSE);
}
expectedValues.add(ownerExternalIdStored);
expectedValues.add(loanId == null ? null : String.valueOf(loanId));
expectedValues.add(loanExternalId);
expectedValues.add(transferExternalId);
List<List<String>> actualValuesList = content.stream().map(t -> {
List<String> actualValues = new ArrayList<>();
actualValues.add(t.getSettlementDate() == null ? null : FORMATTER.format(t.getSettlementDate()));
@@ -354,6 +349,31 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
actualValues.add(t.getEffectiveFrom() == null ? null : FORMATTER.format(t.getEffectiveFrom()));
actualValues.add(t.getEffectiveTo() == null ? null : FORMATTER.format(t.getEffectiveTo()));
actualValues.add(transactionType);
if (expectedValues.size() > 6) {
actualValues.add(
t.getDetails() != null ? t.getDetails().getTotalOutstanding().setScale(2, RoundingMode.HALF_DOWN).toString()
: null);
}
if (expectedValues.size() > 7) {
actualValues.add(t.getDetails() != null
? t.getDetails().getTotalPrincipalOutstanding().setScale(2, RoundingMode.HALF_DOWN).toString()
: null);
}
if (expectedValues.size() > 8) {
actualValues.add(t.getDetails() != null
? t.getDetails().getTotalInterestOutstanding().setScale(2, RoundingMode.HALF_DOWN).toString()
: null);
}
if (expectedValues.size() > 9) {
actualValues.add(t.getDetails() != null
? t.getDetails().getTotalFeeChargesOutstanding().setScale(2, RoundingMode.HALF_DOWN).toString()
: null);
}
if (expectedValues.size() > 10) {
actualValues.add(t.getDetails() != null
? t.getDetails().getTotalPenaltyChargesOutstanding().setScale(2, RoundingMode.HALF_DOWN).toString()
: null);
}
actualValues.add(t.getOwner().getExternalId() == null ? null : t.getOwner().getExternalId());
actualValues.add(loanId == null ? null : String.valueOf(t.getLoan().getLoanId()));
actualValues.add(loanExternalId == null ? null : t.getLoan().getExternalId());
@@ -361,6 +381,11 @@ public class AssetExternalizationStepDef extends AbstractStepDef {
return actualValues;
}).collect(Collectors.toList());
expectedValues.add(ownerExternalIdStored);
expectedValues.add(loanId == null ? null : String.valueOf(loanId));
expectedValues.add(loanExternalId);
expectedValues.add(transferExternalId);
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
assertThat(numberOfElementsActual)
@@ -1679,3 +1679,100 @@ Feature: Asset Externalization
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
@TestRailId:C3690 @AssetExternalizationJournalEntry
Scenario: Verify that Asset externalization SALES and BUYBACK has the correct Journal entries with PAYABLE_OUTSTANDING_INTEREST strategy
When Global config "outstanding-interest-calculation-strategy-for-external-asset-transfer" value set to "PAYABLE_OUTSTANDING_INTEREST"
When Admin sets the business date to "1 May 2023"
When Admin creates a client with random data
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL | 01 May 2023 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "1 May 2023" with "1000" amount and expected disbursement date on "1 May 2023"
When Admin successfully disburse the loan on "1 May 2023" with "1000" EUR transaction amount
Then Loan status will be "ACTIVE"
When Admin makes asset externalization request by Loan ID with unique ownerExternalId, system-generated transferExternalId and the following data:
| Transaction type | settlementDate | purchasePriceRatio |
| sale | 2023-05-21 | 1 |
Then Asset externalization response has the correct Loan ID, transferExternalId
Then Fetching Asset externalization details by loan id gives numberOfElements: 1 with correct ownerExternalId and the following data:
| settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | totalOutstanding | totalPrincipalOutstanding | totalInterestOutstanding | totalFeeChargesOutstanding | totalPenaltyChargesOutstanding |
| 2023-05-21 | 1 | PENDING | 2023-05-01 | 9999-12-31 | SALE | | | | | |
When Admin adds "LOAN_SNOOZE_FEE" due date charge with "10 May 2023" due date and 10 EUR transaction amount
When Admin sets the business date to "10 May 2023"
When Admin runs inline COB job for Loan
When Admin sets the business date to "22 May 2023"
When Admin runs inline COB job for Loan
Then LoanOwnershipTransferBusinessEvent is created
Then LoanAccountSnapshotBusinessEvent is created
Then Fetching Asset externalization details by loan id gives numberOfElements: 2 with correct ownerExternalId and the following data:
| settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | totalOutstanding | totalPrincipalOutstanding | totalInterestOutstanding | totalFeeChargesOutstanding | totalPenaltyChargesOutstanding |
| 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | | | | |
| 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | 1016.58 | 1000.00 | 6.58 | 10.00 | 0.00 |
Then The latest asset externalization transaction with "ACTIVE" status has the following TRANSFER Journal entries:
| glAccountType | glAccountCode | glAccountName | entryType | amount |
| ASSET | 112601 | Loans Receivable | CREDIT | 1000.00 |
| ASSET | 112603 | Interest/Fee Receivable | CREDIT | 16.58 |
| ASSET | 146000 | Asset transfer | DEBIT | 1016.58 |
| ASSET | 112601 | Loans Receivable | DEBIT | 1000.00 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 16.58 |
| ASSET | 146000 | Asset transfer | CREDIT | 1016.58 |
Then The asset external owner has the following OWNER Journal entries:
| glAccountType | glAccountCode | glAccountName | entryType | amount |
| ASSET | 112601 | Loans Receivable | DEBIT | 1000.00 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 16.58 |
When Admin makes asset externalization request by Loan ID with unique ownerExternalId, system-generated transferExternalId and the following data:
| Transaction type | settlementDate | purchasePriceRatio |
| buyback | 2023-05-30 | |
Then Fetching Asset externalization details by loan id gives numberOfElements: 3 with correct ownerExternalId and the following data:
| settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | totalOutstanding | totalPrincipalOutstanding | totalInterestOutstanding | totalFeeChargesOutstanding | totalPenaltyChargesOutstanding |
| 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | | | | |
| 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 9999-12-31 | SALE | 1016.58 | 1000.00 | 6.58 | 10.00 | 0.00 |
| 2023-05-30 | 1 | BUYBACK | 2023-05-22 | 9999-12-31 | BUYBACK | | | | | |
When Admin adds "LOAN_NSF_FEE" due date charge with "25 May 2023" due date and 20 EUR transaction amount
When Admin sets the business date to "26 May 2023"
When Admin runs inline COB job for Loan
When Admin sets the business date to "31 May 2023"
When Admin runs inline COB job for Loan
Then LoanOwnershipTransferBusinessEvent is created
Then LoanAccountSnapshotBusinessEvent is created
Then Fetching Asset externalization details by loan id gives numberOfElements: 3 with correct ownerExternalId and the following data:
| settlementDate | purchasePriceRatio | status | effectiveFrom | effectiveTo | Transaction type | totalOutstanding | totalPrincipalOutstanding | totalInterestOutstanding | totalFeeChargesOutstanding | totalPenaltyChargesOutstanding |
| 2023-05-21 | 1 | PENDING | 2023-05-01 | 2023-05-21 | SALE | | | | | |
| 2023-05-21 | 1 | ACTIVE | 2023-05-22 | 2023-05-30 | SALE | 1016.58 | 1000.00 | 6.58 | 10.00 | 0.00 |
| 2023-05-30 | 1 | BUYBACK | 2023-05-22 | 2023-05-30 | BUYBACK | 1039.53 | 1000.00 | 9.53 | 10.00 | 20.00 |
Then The latest asset externalization transaction with "BUYBACK" status has the following TRANSFER Journal entries:
| glAccountType | glAccountCode | glAccountName | entryType | amount |
| ASSET | 112601 | Loans Receivable | DEBIT | 1000.00 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 39.53 |
| ASSET | 146000 | Asset transfer | CREDIT | 1039.53 |
| ASSET | 112601 | Loans Receivable | CREDIT | 1000.00 |
| ASSET | 112603 | Interest/Fee Receivable | CREDIT | 39.53 |
| ASSET | 146000 | Asset transfer | DEBIT | 1039.53 |
Then The asset external owner has the following OWNER Journal entries:
| glAccountType | glAccountCode | glAccountName | entryType | amount |
| ASSET | 112601 | Loans Receivable | DEBIT | 1000.00 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 16.58 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
| ASSET | 112603 | Interest/Fee Receivable | DEBIT | 0.33 |
| INCOME | 404000 | Interest Income | CREDIT | 0.33 |
When Global config "outstanding-interest-calculation-strategy-for-external-asset-transfer" value set to "TOTAL_OUTSTANDING_INTEREST"
@@ -21,7 +21,6 @@ package org.apache.fineract.investor.cob.loan;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
@@ -42,6 +41,7 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
import org.apache.fineract.investor.service.AccountingService;
import org.apache.fineract.investor.service.DelayedSettlementAttributeService;
import org.apache.fineract.investor.service.ExternalAssetOwnerTransferOutstandingInterestCalculation;
import org.apache.fineract.investor.service.LoanTransferabilityService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.context.annotation.Conditional;
@@ -65,6 +65,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransferabilityService loanTransferabilityService;
private final DelayedSettlementAttributeService delayedSettlementAttributeService;
private final ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation;
@Override
public Loan execute(Loan loan) {
@@ -220,14 +221,13 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer);
details.setTotalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO));
details.setTotalPrincipalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO));
details.setTotalInterestOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
details.setTotalFeeChargesOutstanding(
Objects.requireNonNullElse(loan.getSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO));
details.setTotalPenaltyChargesOutstanding(
Objects.requireNonNullElse(loan.getSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO));
details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO));
details.setTotalPrincipalOutstanding(loan.getSummary().getTotalPrincipalOutstanding());
// We have different strategies to calculate oustanding interest
final BigDecimal interestAmount = externalAssetOwnerTransferOutstandingInterestCalculation.calculateOutstandingInterest(loan);
details.setTotalInterestOutstanding(interestAmount);
details.setTotalFeeChargesOutstanding(loan.getSummary().getTotalFeeChargesOutstanding());
details.setTotalPenaltyChargesOutstanding(loan.getSummary().getTotalPenaltyChargesOutstanding());
details.setTotalOverpaid(loan.getTotalOverpaid());
return details;
}
@@ -0,0 +1,28 @@
/**
* 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.investor.domain;
public interface AttributeKey {
String getKey();
String getValue();
String getDescription();
}
@@ -0,0 +1,38 @@
/**
* 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.investor.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class ExternalAssetOwnerLoanProductAttribute implements AttributeKey {
public static final ExternalAssetOwnerLoanProductAttribute TOTAL_OUTSTANDING_INTEREST_STRATEGY = new ExternalAssetOwnerLoanProductAttribute(
"OUTSTANDING_INTEREST_STRATEGY", "TOTAL_OUTSTANDING",
"During external owner transfer the total (due + not yet due + projected) interest participate");
public static final ExternalAssetOwnerLoanProductAttribute PAYABLE_OUTSTANDING_INTEREST_STRATEGY = new ExternalAssetOwnerLoanProductAttribute(
"OUTSTANDING_INTEREST_STRATEGY", "PAYABLE_OUTSTANDING",
"During external owner transfer the total (due + not yet due) interest participate");
private final String key;
private final String value;
private final String description;
}
@@ -25,18 +25,20 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.util.Objects;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.MathUtil;
@Getter
@Setter
@Table(name = "m_external_asset_owner_transfer_details")
@NoArgsConstructor
@Entity
public class ExternalAssetOwnerTransferDetails extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Setter
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "asset_owner_transfer_id", referencedColumnName = "id")
private ExternalAssetOwnerTransfer externalAssetOwnerTransfer;
@@ -58,4 +60,33 @@ public class ExternalAssetOwnerTransferDetails extends AbstractAuditableWithUTCD
@Column(name = "total_overpaid_derived", scale = 6, precision = 19, nullable = false)
private BigDecimal totalOverpaid;
public void setTotalPrincipalOutstanding(BigDecimal totalPrincipalOutstanding) {
this.totalPrincipalOutstanding = Objects.requireNonNullElse(totalPrincipalOutstanding, BigDecimal.ZERO);
updateTotalOutstanding();
}
public void setTotalInterestOutstanding(BigDecimal totalInterestOutstanding) {
this.totalInterestOutstanding = Objects.requireNonNullElse(totalInterestOutstanding, BigDecimal.ZERO);
updateTotalOutstanding();
}
public void setTotalFeeChargesOutstanding(BigDecimal totalFeeChargesOutstanding) {
this.totalFeeChargesOutstanding = Objects.requireNonNullElse(totalFeeChargesOutstanding, BigDecimal.ZERO);
updateTotalOutstanding();
}
public void setTotalPenaltyChargesOutstanding(BigDecimal totalPenaltyChargesOutstanding) {
this.totalPenaltyChargesOutstanding = Objects.requireNonNullElse(totalPenaltyChargesOutstanding, BigDecimal.ZERO);
updateTotalOutstanding();
}
private void updateTotalOutstanding() {
this.totalOutstanding = MathUtil.add(getTotalPrincipalOutstanding(), getTotalInterestOutstanding(), getTotalFeeChargesOutstanding(),
getTotalPenaltyChargesOutstanding());
}
public void setTotalOverpaid(BigDecimal totalOverpaid) {
this.totalOverpaid = Objects.requireNonNullElse(totalOverpaid, BigDecimal.ZERO);
}
}
@@ -55,6 +55,7 @@ public class AccountingServiceImpl implements AccountingService {
private final ExternalAssetOwnerTransferJournalEntryMappingRepository externalAssetOwnerTransferJournalEntryMappingRepository;
private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
private final ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation;
@Override
public void createJournalEntriesForSaleAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer,
@@ -104,7 +105,8 @@ public class AccountingServiceImpl implements AccountingService {
final Long transactionId = transfer.getId();
final LocalDate transactionDate = transfer.getSettlementDate();
final BigDecimal principalAmount = loan.getSummary().getTotalPrincipalOutstanding();
final BigDecimal interestAmount = loan.getSummary().getTotalInterestOutstanding();
// We have different strategies to calculate oustanding interest
final BigDecimal interestAmount = externalAssetOwnerTransferOutstandingInterestCalculation.calculateOutstandingInterest(loan);
final BigDecimal feesAmount = loan.getSummary().getTotalFeeChargesOutstanding();
final BigDecimal penaltiesAmount = loan.getSummary().getTotalPenaltyChargesOutstanding();
final BigDecimal overPaymentAmount = loan.getTotalOverpaid();
@@ -0,0 +1,69 @@
/**
* 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.investor.service;
import java.math.BigDecimal;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.investor.config.InvestorModuleIsEnabledCondition;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryDataProvider;
import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryProviderDelegate;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Conditional(InvestorModuleIsEnabledCondition.class)
public class ExternalAssetOwnerTransferOutstandingInterestCalculation {
private final LoanSummaryProviderDelegate loanSummaryDataProvider;
private final ConfigurationDomainService configurationDomainService;
private final LoanReadPlatformService loanReadPlatformService;
private LoanSummaryDataProvider fetchLoanSummaryDataProvider(Loan loan) {
return this.loanSummaryDataProvider.resolveLoanSummaryDataProvider(loan.getTransactionProcessingStrategyCode());
}
public BigDecimal calculateOutstandingInterest(Loan loan) {
String outstandingInterestCalculationStrategy = configurationDomainService.getAssetOwnerTransferOustandingInterestStrategy();
return switch (outstandingInterestCalculationStrategy) {
case "TOTAL_OUTSTANDING_INTEREST" -> loan.getSummary().getTotalInterestOutstanding();
case "PAYABLE_OUTSTANDING_INTEREST" -> {
LoanAccountData data = loanReadPlatformService.retrieveOne(loan.getId());
data = loanReadPlatformService.fetchRepaymentScheduleData(data);
Money duePayableAmount = loan
.getRepaymentScheduleInstallments(i -> !i.getDueDate().isAfter(DateUtils.getBusinessLocalDate())).stream()
.map(i -> i.getInterestOutstanding(loan.getCurrency())).reduce(Money.zero(loan.getCurrency()), MathUtil::plus);
BigDecimal notDuePayableAmount = fetchLoanSummaryDataProvider(loan)
.computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan, data.getRepaymentSchedule().getPeriods(),
DateUtils.getBusinessLocalDate(), loan.getCurrency().toData(), duePayableAmount.getAmount());
yield MathUtil.add(duePayableAmount.getAmount(), notDuePayableAmount);
}
default -> throw new UnsupportedOperationException(
"Unknown outstanding interest calculation: " + outstandingInterestCalculationStrategy);
};
}
}
@@ -35,7 +35,6 @@ import static org.apache.fineract.investor.data.ExternalTransferSubStatus.UNSOLD
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.MathUtil;
@@ -63,6 +62,7 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
private final AccountingService accountingService;
private final BusinessEventNotifierService businessEventNotifierService;
private final ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation;
@Override
public void handleLoanClosedOrOverpaid(Loan loan) {
@@ -166,14 +166,13 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer);
details.setTotalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO));
details.setTotalPrincipalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO));
details.setTotalInterestOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
details.setTotalFeeChargesOutstanding(
Objects.requireNonNullElse(loan.getSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO));
details.setTotalPenaltyChargesOutstanding(
Objects.requireNonNullElse(loan.getSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO));
details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO));
details.setTotalPrincipalOutstanding(loan.getSummary().getTotalPrincipalOutstanding());
// We have different strategies to calculate oustanding interest
final BigDecimal interestAmount = externalAssetOwnerTransferOutstandingInterestCalculation.calculateOutstandingInterest(loan);
details.setTotalInterestOutstanding(interestAmount);
details.setTotalFeeChargesOutstanding(loan.getSummary().getTotalFeeChargesOutstanding());
details.setTotalPenaltyChargesOutstanding(loan.getSummary().getTotalPenaltyChargesOutstanding());
details.setTotalOverpaid(loan.getTotalOverpaid());
return details;
}
@@ -39,4 +39,5 @@
<include relativeToChangelogFile="true" file="parts/0015_add_intermediary_sale_command.xml"/>
<include relativeToChangelogFile="true" file="parts/0016_add_external_reference_id.xml"/>
<include relativeToChangelogFile="true" file="parts/0017_add_external_asset_owner_loan_product_attr_index.xml"/>
<include relativeToChangelogFile="true" file="parts/0018_add_external_asset_owner_transfer_outstanding_interest_strategy.xml"/>
</databaseChangeLog>
@@ -0,0 +1,43 @@
<?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
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="fineract" id="1" context="postgresql">
<sql>
SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, false ) FROM c_configuration;
</sql>
</changeSet>
<changeSet author="fineract" id="2">
<insert tableName="c_configuration">
<column name="name" value="outstanding-interest-calculation-strategy-for-external-asset-transfer"/>
<column name="value"/>
<column name="date_value"/>
<column name="string_value" value="TOTAL_OUTSTANDING_INTEREST"/>
<column name="enabled" valueBoolean="true"/>
<column name="is_trap_door" valueBoolean="false"/>
<column name="description" value=" Available options: TOTAL_OUTSTANDING_INTEREST, PAYABLE_OUTSTANDING_INTEREST. Define whether total outstanding interest (due + not yet due + projected) or payable till transfer date ( due + not yet due) should be involved in the asset transfer."/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -31,6 +32,8 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
@@ -55,12 +58,16 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
import org.apache.fineract.investor.service.AccountingService;
import org.apache.fineract.investor.service.DelayedSettlementAttributeService;
import org.apache.fineract.investor.service.ExternalAssetOwnerTransferOutstandingInterestCalculation;
import org.apache.fineract.investor.service.LoanTransferabilityService;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -69,6 +76,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Sort;
@@ -80,6 +88,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
private static final Long LOAN_PRODUCT_ID = 2L;
private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
@Mock
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
@@ -99,8 +108,22 @@ public class LoanAccountOwnerTransferBusinessStepTest {
@Mock
private DelayedSettlementAttributeService delayedSettlementAttributeService;
@Mock
private ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation;
private LoanAccountOwnerTransferBusinessStep underTest;
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
public static void destruct() {
MONEY_HELPER.close();
}
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
@@ -108,7 +131,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService,
loanTransferabilityService, delayedSettlementAttributeService);
loanTransferabilityService, delayedSettlementAttributeService, externalAssetOwnerTransferOutstandingInterestCalculation);
}
@AfterEach
@@ -27,11 +27,14 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
@@ -51,16 +54,20 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -68,6 +75,19 @@ import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class AccountingServiceImplTest {
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
public static void destruct() {
MONEY_HELPER.close();
}
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BUSINESS_DATE, LocalDate.of(2024, 9, 27))));
@@ -517,6 +537,9 @@ class AccountingServiceImplTest {
@Mock
private FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
@Mock
private ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation;
@InjectMocks
private AccountingServiceImpl testSubject;
@@ -540,7 +563,6 @@ class AccountingServiceImplTest {
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanSummary.getTotalPrincipalOutstanding()).thenReturn(BigDecimal.ONE);
when(loanSummary.getTotalInterestOutstanding()).thenReturn(BigDecimal.ONE);
when(loanSummary.getTotalFeeChargesOutstanding()).thenReturn(BigDecimal.ONE);
when(loanSummary.getTotalPenaltyChargesOutstanding()).thenReturn(BigDecimal.ONE);
when(loan.getSummary()).thenReturn(loanSummary);
@@ -23,11 +23,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.data.domain.Sort.Direction.ASC;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
@@ -44,9 +47,12 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -54,7 +60,9 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Sort;
@@ -63,6 +71,7 @@ import org.springframework.data.jpa.domain.Specification;
@ExtendWith(MockitoExtension.class)
public class LoanAccountOwnerTransferServiceTest {
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
@Mock
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
@Mock
@@ -71,15 +80,27 @@ public class LoanAccountOwnerTransferServiceTest {
private AccountingService accountingService;
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Mock
private ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation;
private LoanAccountOwnerTransferService underTest;
@InjectMocks
private LoanAccountOwnerTransferServiceImpl underTest;
private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
@BeforeAll
public static void init() {
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
public static void destruct() {
MONEY_HELPER.close();
}
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BUSINESS_DATE, actualDate)));
underTest = new LoanAccountOwnerTransferServiceImpl(externalAssetOwnerTransferRepository,
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService);
}
@Test
@@ -42,14 +42,13 @@ import org.apache.fineract.portfolio.account.data.PortfolioAccountData;
import org.apache.fineract.portfolio.accountdetails.data.LoanAccountSummaryData;
import org.apache.fineract.portfolio.calendar.data.CalendarData;
import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.charge.util.ConvertChargeDataToSpecificChargeData;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.group.data.GroupGeneralData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.guarantor.data.GuarantorData;
import org.apache.fineract.portfolio.loanaccount.guarantor.data.IGuarantor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductBorrowerCycleVariationData;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
@@ -146,7 +145,7 @@ public class LoanAccountData {
private Collection<LoanTransactionData> transactions;
private Collection<LoanChargeData> charges;
private Collection<LoanCollateralManagementData> collateral;
private Collection<GuarantorData> guarantors;
private Collection<? extends IGuarantor> guarantors;
private CalendarData meeting;
private Collection<NoteData> notes;
private Collection<DisbursementData> disbursementDetails;
@@ -356,7 +355,7 @@ public class LoanAccountData {
final Collection<LoanChargeData> charges = new ArrayList<LoanChargeData>();
for (final ChargeData charge : product.charges()) {
if (!charge.isOverdueInstallmentCharge()) {
charges.add(ConvertChargeDataToSpecificChargeData.toLoanChargeData(charge));
charges.add(toLoanChargeData(charge));
}
}
@@ -530,7 +529,7 @@ public class LoanAccountData {
*/
public LoanAccountData associationsAndTemplate(final LoanScheduleData repaymentSchedule,
final Collection<LoanTransactionData> transactions, final Collection<LoanChargeData> charges,
final Collection<LoanCollateralManagementData> collateral, final Collection<GuarantorData> guarantors,
final Collection<LoanCollateralManagementData> collateral, final Collection<? extends IGuarantor> guarantors,
final CalendarData calendarData, final Collection<LoanProductData> productOptions,
final Collection<EnumOptionData> termFrequencyTypeOptions, final Collection<EnumOptionData> repaymentFrequencyTypeOptions,
final Collection<EnumOptionData> repaymentFrequencyNthDayTypeOptions,
@@ -680,4 +679,17 @@ public class LoanAccountData {
public boolean isActive() {
return LoanStatus.fromInt(getStatus().getId().intValue()).isActive();
}
public static LoanChargeData toLoanChargeData(final ChargeData chargeData) {
BigDecimal percentage = null;
if (chargeData.getChargeCalculationType().getId() == 2) {
percentage = chargeData.getAmount();
}
return LoanChargeData.newLoanChargeDetails(chargeData.getId(), chargeData.getName(), chargeData.getCurrency(),
chargeData.getAmount(), percentage, chargeData.getChargeTimeType(), chargeData.getChargeCalculationType(),
chargeData.isPenalty(), chargeData.getChargePaymentMode(), chargeData.getMinCap(), chargeData.getMaxCap(),
ExternalId.empty());
}
}
@@ -0,0 +1,23 @@
/**
* 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.guarantor.data;
import java.io.Serializable;
public interface IGuarantor extends Serializable {}
@@ -44,6 +44,7 @@ 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.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -69,6 +70,11 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapperTest {
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.HALF_EVEN));
}
@AfterAll
public static void destruct() {
MONEY_HELPER.close();
}
@Test
public void testOnePeriodWithFeeCharge() {
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
@@ -527,4 +527,10 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
public boolean isImmediateChargeAccrualPostMaturityEnabled() {
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY).isEnabled();
}
@Override
public String getAssetOwnerTransferOustandingInterestStrategy() {
return getGlobalConfigurationPropertyData(
GlobalConfigurationConstants.ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY).getStringValue();
}
}
@@ -21,9 +21,7 @@ package org.apache.fineract.portfolio.charge.util;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.shareaccounts.data.ShareAccountChargeData;
@@ -31,19 +29,6 @@ public final class ConvertChargeDataToSpecificChargeData {
private ConvertChargeDataToSpecificChargeData() {}
public static LoanChargeData toLoanChargeData(final ChargeData chargeData) {
BigDecimal percentage = null;
if (chargeData.getChargeCalculationType().getId() == 2) {
percentage = chargeData.getAmount();
}
return LoanChargeData.newLoanChargeDetails(chargeData.getId(), chargeData.getName(), chargeData.getCurrency(),
chargeData.getAmount(), percentage, chargeData.getChargeTimeType(), chargeData.getChargeCalculationType(),
chargeData.isPenalty(), chargeData.getChargePaymentMode(), chargeData.getMinCap(), chargeData.getMaxCap(),
ExternalId.empty());
}
public static SavingsAccountChargeData toSavingsAccountChargeData(final ChargeData chargeData) {
final Long savingsChargeId = null;
@@ -19,7 +19,6 @@
package org.apache.fineract.portfolio.loanaccount.guarantor.data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
@@ -34,7 +33,7 @@ import org.apache.fineract.portfolio.loanaccount.guarantor.domain.GuarantorType;
import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorEnumerations;
@Getter
public class GuarantorData implements Serializable {
public class GuarantorData implements IGuarantor {
@Serial
private static final long serialVersionUID = 1L;
@@ -105,8 +105,8 @@ public class GlobalConfigurationHelper {
ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
GetGlobalConfigurationsResponse actualGlobalConfigurations = getAllGlobalConfigurations();
Assertions.assertEquals(56, expectedGlobalConfigurations.size());
Assertions.assertEquals(56, actualGlobalConfigurations.getGlobalConfiguration().size());
Assertions.assertEquals(57, expectedGlobalConfigurations.size());
Assertions.assertEquals(57, actualGlobalConfigurations.getGlobalConfiguration().size());
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
@@ -540,6 +540,15 @@ public class GlobalConfigurationHelper {
enableImmediateChargeAccrualPostMaturity.put("trapDoor", false);
defaults.add(enableImmediateChargeAccrualPostMaturity);
HashMap<String, Object> assetOwnerTransferInterestOutstandingStrategy = new HashMap<>();
assetOwnerTransferInterestOutstandingStrategy.put("name",
GlobalConfigurationConstants.ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY);
assetOwnerTransferInterestOutstandingStrategy.put("value", 0L);
assetOwnerTransferInterestOutstandingStrategy.put("enabled", false);
assetOwnerTransferInterestOutstandingStrategy.put("trapDoor", false);
assetOwnerTransferInterestOutstandingStrategy.put("string_value", "TOTAL_OUTSTANDING_INTEREST");
defaults.add(assetOwnerTransferInterestOutstandingStrategy);
return defaults;
}