Skip to content

Commit 84f7c92

Browse files
Jose Alberto Hernandezgalovics
authored andcommitted
FINERACT-2232: Capitalized Income - Fetch amortization details
1 parent c0859f4 commit 84f7c92

11 files changed

Lines changed: 272 additions & 0 deletions

File tree

fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
import org.apache.fineract.client.services.LoanChargesApi;
8989
import org.apache.fineract.client.services.LoanCobCatchUpApi;
9090
import org.apache.fineract.client.services.LoanCollateralApi;
91+
import org.apache.fineract.client.services.LoanDeferredIncomeDataApi;
9192
import org.apache.fineract.client.services.LoanDisbursementDetailsApi;
9293
import org.apache.fineract.client.services.LoanInterestPauseApi;
9394
import org.apache.fineract.client.services.LoanProductsApi;
@@ -230,6 +231,7 @@ public final class FineractClient {
230231
public final LoanChargesApi loanCharges;
231232
public final LoanCobCatchUpApi loanCobCatchUpApi;
232233
public final LoanCollateralApi loanCollaterals;
234+
public final LoanDeferredIncomeDataApi loanDeferredIncome;
233235
public final LoanProductsApi loanProducts;
234236
public final LoanReschedulingApi loanSchedules;
235237
public final LoansPointInTimeApi loansPointInTimeApi;
@@ -359,6 +361,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
359361
loanCharges = retrofit.create(LoanChargesApi.class);
360362
loanCobCatchUpApi = retrofit.create(LoanCobCatchUpApi.class);
361363
loanCollaterals = retrofit.create(LoanCollateralApi.class);
364+
loanDeferredIncome = retrofit.create(LoanDeferredIncomeDataApi.class);
362365
loanProducts = retrofit.create(LoanProductsApi.class);
363366
loanSchedules = retrofit.create(LoanReschedulingApi.class);
364367
loansPointInTimeApi = retrofit.create(LoansPointInTimeApi.class);

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,7 @@ List<Loan> findLoansForAddAccrual(@Param("accountingType") AccountingRuleType ac
268268

269269
@Query(FIND_LOAN_BY_EXTERNAL_ID)
270270
Optional<Loan> findByExternalId(@Param("externalId") ExternalId externalId);
271+
272+
@Query("select loan.loanRepaymentScheduleDetail.enableIncomeCapitalization from Loan loan where loan.id = :loanId")
273+
Boolean isEnabledCapitalizedIncome(Long loanId);
271274
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,8 @@ public List<Long> findIdByExternalIds(List<ExternalId> externalIds) {
309309
public boolean existsByLoanId(Long loanId) {
310310
return repository.existsById(loanId);
311311
}
312+
313+
public boolean isEnabledCapitalizedIncome(Long loanId) {
314+
return repository.isEnabledCapitalizedIncome(loanId);
315+
}
312316
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.api;
20+
21+
import io.swagger.v3.oas.annotations.Operation;
22+
import io.swagger.v3.oas.annotations.Parameter;
23+
import io.swagger.v3.oas.annotations.tags.Tag;
24+
import jakarta.ws.rs.Consumes;
25+
import jakarta.ws.rs.GET;
26+
import jakarta.ws.rs.Path;
27+
import jakarta.ws.rs.PathParam;
28+
import jakarta.ws.rs.Produces;
29+
import jakarta.ws.rs.core.Context;
30+
import jakarta.ws.rs.core.MediaType;
31+
import jakarta.ws.rs.core.UriInfo;
32+
import lombok.RequiredArgsConstructor;
33+
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
34+
import org.apache.fineract.portfolio.loanaccount.data.LoanDeferredIncomeData;
35+
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceReadService;
36+
import org.springframework.stereotype.Component;
37+
38+
@Path("/v1/loans/{loanId}/deferredincome")
39+
@Component
40+
@Tag(name = "Loan Deferred Income data", description = "Loan deferred income like Capitalized Income to fetch the Deferred Income related informations")
41+
@RequiredArgsConstructor
42+
public class LoanDeferredIncomeApiResource {
43+
44+
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOAN";
45+
private final PlatformSecurityContext context;
46+
private final CapitalizedIncomeBalanceReadService capitalizedIncomeBalanceReadService;
47+
48+
@GET
49+
@Consumes({ MediaType.APPLICATION_JSON })
50+
@Produces({ MediaType.APPLICATION_JSON })
51+
@Operation(summary = "Fetch the Capitalized Income related informations")
52+
public LoanDeferredIncomeData fetchDeferredIncomeDetails(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
53+
@Context final UriInfo uriInfo) {
54+
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
55+
56+
return capitalizedIncomeBalanceReadService.fetchLoanDeferredIncomeData(loanId);
57+
}
58+
59+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.data;
20+
21+
import java.math.BigDecimal;
22+
import lombok.AllArgsConstructor;
23+
import lombok.Getter;
24+
import lombok.Setter;
25+
import lombok.ToString;
26+
27+
@AllArgsConstructor
28+
@ToString
29+
@Getter
30+
@Setter
31+
public class CapitalizedIncomeDetails {
32+
33+
private BigDecimal amount;
34+
private BigDecimal amortizedAmount;
35+
private BigDecimal unrecognizedAmount;
36+
private BigDecimal amountAdjustment;
37+
private BigDecimal chargedOffAmount;
38+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.data;
20+
21+
import java.util.List;
22+
import lombok.AllArgsConstructor;
23+
import lombok.Getter;
24+
import lombok.NoArgsConstructor;
25+
import lombok.Setter;
26+
import lombok.ToString;
27+
28+
@AllArgsConstructor
29+
@NoArgsConstructor
30+
@ToString
31+
@Getter
32+
@Setter
33+
public class LoanDeferredIncomeData {
34+
35+
private List<CapitalizedIncomeDetails> capitalizedIncomeData;
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.service;
20+
21+
import org.apache.fineract.portfolio.loanaccount.data.LoanDeferredIncomeData;
22+
23+
public interface CapitalizedIncomeBalanceReadService {
24+
25+
LoanDeferredIncomeData fetchLoanDeferredIncomeData(Long loanId);
26+
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.portfolio.loanaccount.service;
20+
21+
import java.math.BigDecimal;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import lombok.RequiredArgsConstructor;
25+
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
26+
import org.apache.fineract.infrastructure.core.service.MathUtil;
27+
import org.apache.fineract.portfolio.loanaccount.data.CapitalizedIncomeDetails;
28+
import org.apache.fineract.portfolio.loanaccount.data.LoanDeferredIncomeData;
29+
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
30+
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
31+
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
32+
import org.springframework.transaction.annotation.Transactional;
33+
34+
@RequiredArgsConstructor
35+
public class CapitalizedIncomeBalanceReadServiceImpl implements CapitalizedIncomeBalanceReadService {
36+
37+
private final LoanRepositoryWrapper loanRepository;
38+
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
39+
40+
@Override
41+
@Transactional
42+
public LoanDeferredIncomeData fetchLoanDeferredIncomeData(final Long loanId) {
43+
if (loanRepository.isEnabledCapitalizedIncome(loanId)) {
44+
45+
List<CapitalizedIncomeDetails> capitalizedIncomeData = new ArrayList<>();
46+
List<LoanCapitalizedIncomeBalance> capitalizedIncomeBalances = capitalizedIncomeBalanceRepository.findAllByLoanId(loanId);
47+
for (final LoanCapitalizedIncomeBalance capitalizedIncomeBalance : capitalizedIncomeBalances) {
48+
final BigDecimal amortizedAmount = capitalizedIncomeBalance.getAmount() //
49+
.subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getUnrecognizedAmount())) //
50+
.subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getAmountAdjustment())) //
51+
.subtract(MathUtil.nullToZero(capitalizedIncomeBalance.getChargedOffAmount()));
52+
53+
capitalizedIncomeData.add(new CapitalizedIncomeDetails(capitalizedIncomeBalance.getAmount(), amortizedAmount,
54+
capitalizedIncomeBalance.getUnrecognizedAmount(), //
55+
capitalizedIncomeBalance.getAmountAdjustment(), //
56+
capitalizedIncomeBalance.getChargedOffAmount()));
57+
}
58+
59+
return new LoanDeferredIncomeData(capitalizedIncomeData);
60+
}
61+
throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.enabled.capitalized.income",
62+
"Loan: " + loanId + " is not enabled Capitalized Income feature", loanId);
63+
}
64+
65+
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeeBalanceRepository;
2828
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
2929
import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
30+
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceReadService;
31+
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceReadServiceImpl;
3032
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceService;
3133
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceServiceImpl;
3234
import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService;
@@ -77,4 +79,11 @@ public CapitalizedIncomeBalanceService capitalizedIncomeBalanceService(
7779
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) {
7880
return new CapitalizedIncomeBalanceServiceImpl(loanCapitalizedIncomeBalanceRepository);
7981
}
82+
83+
@Bean
84+
@ConditionalOnMissingBean(CapitalizedIncomeBalanceReadService.class)
85+
public CapitalizedIncomeBalanceReadService capitalizedIncomeBalanceReadService(LoanRepositoryWrapper loanRepository,
86+
LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) {
87+
return new CapitalizedIncomeBalanceReadServiceImpl(loanRepository, loanCapitalizedIncomeBalanceRepository);
88+
}
8089
}

integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.fineract.integrationtests;
2020

21+
import static org.junit.jupiter.api.Assertions.assertEquals;
2122
import static org.junit.jupiter.api.Assertions.assertNotNull;
2223
import static org.junit.jupiter.api.Assertions.assertTrue;
2324

@@ -26,8 +27,10 @@
2627
import java.time.LocalDate;
2728
import java.util.Optional;
2829
import java.util.concurrent.atomic.AtomicReference;
30+
import org.apache.fineract.client.models.CapitalizedIncomeDetails;
2931
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
3032
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
33+
import org.apache.fineract.client.models.LoanDeferredIncomeData;
3134
import org.apache.fineract.client.models.PostClientsResponse;
3235
import org.apache.fineract.client.models.PostLoanProductsRequest;
3336
import org.apache.fineract.client.models.PostLoanProductsResponse;
@@ -37,6 +40,7 @@
3740
import org.apache.fineract.client.util.CallFailedRuntimeException;
3841
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
3942
import org.apache.fineract.integrationtests.common.ClientHelper;
43+
import org.apache.fineract.integrationtests.common.Utils;
4044
import org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
4145
import org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
4246
import org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent;
@@ -85,6 +89,12 @@ public void testLoanCapitalizedIncomeAmortization() {
8589
transaction(50.0, "Capitalized Income", "01 January 2024"), //
8690
transaction(0.55, "Capitalized Income Amortization", "01 January 2024") //
8791
);
92+
final LoanDeferredIncomeData loanDeferredIncomeData = loanTransactionHelper.fetchDeferredIncomeDetails(loanId);
93+
assertTrue(loanDeferredIncomeData.getCapitalizedIncomeData().size() > 0);
94+
final CapitalizedIncomeDetails capitalizedIncomeData = loanDeferredIncomeData.getCapitalizedIncomeData().get(0);
95+
assertNotNull(capitalizedIncomeData);
96+
assertEquals(50.0, Utils.getDoubleValue(capitalizedIncomeData.getAmount()));
97+
assertEquals(0.55, Utils.getDoubleValue(capitalizedIncomeData.getAmortizedAmount()));
8898
});
8999
runAt("3 January 2024", () -> {
90100
Long loanId = loanIdRef.get();
@@ -97,6 +107,13 @@ public void testLoanCapitalizedIncomeAmortization() {
97107
transaction(0.03, "Accrual", "02 January 2024"), //
98108
transaction(0.55, "Capitalized Income Amortization", "02 January 2024") //
99109
);
110+
final LoanDeferredIncomeData loanDeferredIncomeData = loanTransactionHelper.fetchDeferredIncomeDetails(loanId);
111+
assertTrue(loanDeferredIncomeData.getCapitalizedIncomeData().size() > 0);
112+
final CapitalizedIncomeDetails capitalizedIncomeData = loanDeferredIncomeData.getCapitalizedIncomeData().get(0);
113+
assertNotNull(capitalizedIncomeData);
114+
assertEquals(50.0, Utils.getDoubleValue(capitalizedIncomeData.getAmount()));
115+
assertEquals(1.1, Utils.getDoubleValue(capitalizedIncomeData.getAmortizedAmount()));
116+
assertEquals(48.90, Utils.getDoubleValue(capitalizedIncomeData.getUnrecognizedAmount()));
100117

101118
verifyJournalEntries(loanId, //
102119
journalEntry(100, loansReceivableAccount, "DEBIT"), //
@@ -181,6 +198,12 @@ public void testLoanCapitalizedIncomeAdjustment() {
181198
transaction(50.0, "Capitalized Income", "01 January 2024"), //
182199
transaction(50.0, "Capitalized Income Adjustment", "01 April 2024") //
183200
);
201+
final LoanDeferredIncomeData loanDeferredIncomeData = loanTransactionHelper.fetchDeferredIncomeDetails(loanId);
202+
assertTrue(loanDeferredIncomeData.getCapitalizedIncomeData().size() > 0);
203+
final CapitalizedIncomeDetails capitalizedIncomeData = loanDeferredIncomeData.getCapitalizedIncomeData().get(0);
204+
assertNotNull(capitalizedIncomeData);
205+
assertEquals(50.0, Utils.getDoubleValue(capitalizedIncomeData.getAmount()));
206+
assertEquals(50.0, Utils.getDoubleValue(capitalizedIncomeData.getAmountAdjustment()));
184207

185208
verifyJournalEntries(loanId, //
186209
journalEntry(100, loansReceivableAccount, "DEBIT"), //

0 commit comments

Comments
 (0)