From 18a55874ba769f34b3942a8b8f40728b237d9211 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Wed, 21 May 2025 15:12:37 +0200 Subject: [PATCH] FINERACT-2286: Make configurable the outstanding interest strategy during asset owner transfer --- .../api/GlobalConfigurationConstants.java | 1 + .../domain/ConfigurationDomainService.java | 1 + .../helper/GlobalConfigurationHelper.java | 6 ++ .../messaging/event/EventCheckHelper.java | 21 +++- .../AssetExternalizationStepDef.java | 35 ++++++- .../features/AssetExternalization.feature | 97 +++++++++++++++++++ .../LoanAccountOwnerTransferBusinessStep.java | 18 ++-- .../investor/domain/AttributeKey.java | 28 ++++++ ...xternalAssetOwnerLoanProductAttribute.java | 38 ++++++++ .../ExternalAssetOwnerTransferDetails.java | 33 ++++++- .../service/AccountingServiceImpl.java | 4 +- ...ransferOutstandingInterestCalculation.java | 69 +++++++++++++ .../LoanAccountOwnerTransferServiceImpl.java | 17 ++-- .../investor/module-changelog-master.xml | 1 + ...transfer_outstanding_interest_strategy.xml | 43 ++++++++ ...nAccountOwnerTransferBusinessStepTest.java | 25 ++++- .../service/AccountingServiceImplTest.java | 24 ++++- .../LoanAccountOwnerTransferServiceTest.java | 27 +++++- .../loanaccount/data/LoanAccountData.java | 22 ++++- .../guarantor/data/IGuarantor.java | 23 +++++ .../service/LoanReadPlatformService.java | 0 .../service/LoanSummaryDataProvider.java | 0 .../service/LoanSummaryProviderDelegate.java | 0 ...epaymentScheduleProcessingWrapperTest.java | 6 ++ .../domain/ConfigurationDomainServiceJpa.java | 6 ++ ...ConvertChargeDataToSpecificChargeData.java | 15 --- .../guarantor/data/GuarantorData.java | 3 +- .../common/GlobalConfigurationHelper.java | 13 ++- 28 files changed, 517 insertions(+), 59 deletions(-) create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/domain/AttributeKey.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerLoanProductAttribute.java create mode 100644 fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java create mode 100644 fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0018_add_external_asset_owner_transfer_outstanding_interest_strategy.xml rename {fineract-provider => fineract-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java (98%) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/IGuarantor.java rename {fineract-provider => fineract-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java (100%) rename {fineract-provider => fineract-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java (100%) rename {fineract-provider => fineract-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java (100%) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java index 82d91b498..3e6426a1d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java @@ -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() {} } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index e7482d16a..7a138fb25 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -145,4 +145,5 @@ public interface ConfigurationDomainService { boolean isImmediateChargeAccrualPostMaturityEnabled(); + String getAssetOwnerTransferOustandingInterestStrategy(); } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java index 224b6d76e..fefacea9e 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/GlobalConfigurationHelper.java @@ -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 configuration = globalConfigurationApi.retrieveOneByName(configKey).execute(); + ErrorHelper.checkSuccessfulApiCall(configuration); + return configuration.body(); + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java index abf0c196e..6f7f7397f 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java @@ -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 clientCreationResponse) throws IOException { Response 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 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) diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java index 82d597abc..b9f9fea0d 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/assetexternalization/AssetExternalizationStepDef.java @@ -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> actualValuesList = content.stream().map(t -> { List 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) diff --git a/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature b/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature index 327c9333a..c205844b6 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/AssetExternalization.feature @@ -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" diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java index 752501c9b..3ba0963ee 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java @@ -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; } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/AttributeKey.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/AttributeKey.java new file mode 100644 index 000000000..f874eaf1f --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/AttributeKey.java @@ -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(); +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerLoanProductAttribute.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerLoanProductAttribute.java new file mode 100644 index 000000000..fb08c3820 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerLoanProductAttribute.java @@ -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; +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferDetails.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferDetails.java index da1d29727..13ae8323f 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferDetails.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferDetails.java @@ -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 { + @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); + } } diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java index fe72d6182..d60487a13 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/AccountingServiceImpl.java @@ -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(); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java new file mode 100644 index 000000000..46422b88c --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java @@ -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); + }; + } +} diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java index c638a06d8..28876ff79 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java @@ -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; } diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml index b3e6f32ee..e96557908 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml @@ -39,4 +39,5 @@ + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0018_add_external_asset_owner_transfer_outstanding_interest_strategy.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0018_add_external_asset_owner_transfer_outstanding_interest_strategy.xml new file mode 100644 index 000000000..7004b0636 --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0018_add_external_asset_owner_transfer_outstanding_interest_strategy.xml @@ -0,0 +1,43 @@ + + + + + + + SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, false ) FROM c_configuration; + + + + + + + + + + + + + + diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java index ed5fa3934..39c5db446 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java @@ -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 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 diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/AccountingServiceImplTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/AccountingServiceImplTest.java index 9e20c1e4e..ad2d1816a 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/AccountingServiceImplTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/AccountingServiceImplTest.java @@ -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 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); diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java index 430ff33a9..ac409b363 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java @@ -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 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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java similarity index 98% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index f2e4b2f35..333da377d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -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 transactions; private Collection charges; private Collection collateral; - private Collection guarantors; + private Collection guarantors; private CalendarData meeting; private Collection notes; private Collection disbursementDetails; @@ -356,7 +355,7 @@ public class LoanAccountData { final Collection charges = new ArrayList(); 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 transactions, final Collection charges, - final Collection collateral, final Collection guarantors, + final Collection collateral, final Collection guarantors, final CalendarData calendarData, final Collection productOptions, final Collection termFrequencyTypeOptions, final Collection repaymentFrequencyTypeOptions, final Collection 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()); + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/IGuarantor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/IGuarantor.java new file mode 100644 index 000000000..cb1d91188 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/IGuarantor.java @@ -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 {} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java similarity index 100% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryProviderDelegate.java diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java index b8e124e09..2e8203134 100644 --- a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/SingleLoanChargeRepaymentScheduleProcessingWrapperTest.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index 2e8558184..cc17cad24 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -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(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/util/ConvertChargeDataToSpecificChargeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/util/ConvertChargeDataToSpecificChargeData.java index 8f50d46c1..ac2a6c2aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/util/ConvertChargeDataToSpecificChargeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/charge/util/ConvertChargeDataToSpecificChargeData.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java index ea293c89f..4bdd36bea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java @@ -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; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index bc0e0f297..e636d6346 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -105,8 +105,8 @@ public class GlobalConfigurationHelper { ArrayList 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 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; }