Skip to content

Commit d69241d

Browse files
committed
Fineract-2460: Add Client Performance API for real-time financial metrics
1 parent 5831cb3 commit d69241d

File tree

5 files changed

+164
-1
lines changed

5 files changed

+164
-1
lines changed

fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/ClientsApiResource.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.apache.fineract.portfolio.accountdetails.data.AccountSummaryCollectionData;
6767
import org.apache.fineract.portfolio.accountdetails.service.AccountDetailsReadPlatformService;
6868
import org.apache.fineract.portfolio.client.data.ClientData;
69+
import org.apache.fineract.portfolio.client.data.ClientPerformanceData;
6970
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
7071
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
7172
import org.apache.fineract.portfolio.client.service.ClientTemplateReadPlatformService;
@@ -172,6 +173,23 @@ public String retrieveOne(@PathParam("clientId") @Parameter(description = "clien
172173
return retrieveClient(clientId, null, staffInSelectedOfficeOnly, uriInfo);
173174
}
174175

176+
@GET
177+
@Path("{clientId}/performance")
178+
@Consumes({ MediaType.APPLICATION_JSON })
179+
@Produces({ MediaType.APPLICATION_JSON })
180+
@Operation(summary = "Retrieve client performance metrics", description = "Returns a summary of active loans and outstanding balances for a specific client.")
181+
@ApiResponses({
182+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientPerformanceData.class))) })
183+
public String retrieveClientPerformance(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
184+
@Context final UriInfo uriInfo) {
185+
186+
this.context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
187+
188+
final ClientPerformanceData performanceData = this.clientReadPlatformService.retrieveClientPerformance(clientId);
189+
190+
return this.toApiJsonSerializer.serialize(performanceData);
191+
}
192+
175193
@POST
176194
@Consumes({ MediaType.APPLICATION_JSON })
177195
@Produces({ MediaType.APPLICATION_JSON })
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
20+
package org.apache.fineract.portfolio.client.data;
21+
22+
import java.io.Serializable;
23+
import java.math.BigDecimal;
24+
25+
/**
26+
* Data object for Client Performance metrics.
27+
*/
28+
public final class ClientPerformanceData implements Serializable {
29+
30+
private final Integer activeLoans;
31+
private final BigDecimal totalOutstandingBalance;
32+
33+
// Constructor is private to force use of the static 'instance' method (Fineract style)
34+
private ClientPerformanceData(final Integer activeLoans, final BigDecimal totalOutstandingBalance) {
35+
this.activeLoans = activeLoans;
36+
this.totalOutstandingBalance = totalOutstandingBalance;
37+
}
38+
39+
public static ClientPerformanceData instance(final Integer activeLoans, final BigDecimal totalOutstandingBalance) {
40+
return new ClientPerformanceData(activeLoans, totalOutstandingBalance);
41+
}
42+
43+
// Getters for Spring to convert this to JSON
44+
public Integer getActiveLoans() {
45+
return activeLoans;
46+
}
47+
48+
public BigDecimal getTotalOutstandingBalance() {
49+
return totalOutstandingBalance;
50+
}
51+
}

fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.fineract.infrastructure.core.service.Page;
2525
import org.apache.fineract.infrastructure.core.service.SearchParameters;
2626
import org.apache.fineract.portfolio.client.data.ClientData;
27+
import org.apache.fineract.portfolio.client.data.ClientPerformanceData;
2728

2829
public interface ClientReadPlatformService {
2930

@@ -60,4 +61,6 @@ public interface ClientReadPlatformService {
6061

6162
Long retrieveClientIdByExternalId(ExternalId externalId);
6263

64+
ClientPerformanceData retrieveClientPerformance(Long clientId);
65+
6366
}

fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientReadPlatformServiceImpl.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.apache.fineract.portfolio.client.data.ClientCollateralManagementData;
4848
import org.apache.fineract.portfolio.client.data.ClientData;
4949
import org.apache.fineract.portfolio.client.data.ClientNonPersonData;
50+
import org.apache.fineract.portfolio.client.data.ClientPerformanceData;
5051
import org.apache.fineract.portfolio.client.data.ClientTimelineData;
5152
import org.apache.fineract.portfolio.client.domain.Client;
5253
import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
@@ -601,6 +602,29 @@ public Long retrieveClientIdByExternalId(final ExternalId externalId) {
601602
return clientRepositoryWrapper.findIdByExternalId(externalId);
602603
}
603604

605+
@Override
606+
public ClientPerformanceData retrieveClientPerformance(final Long clientId) {
607+
try {
608+
final String checkClientSql = "SELECT count(*) FROM m_client WHERE id = ?";
609+
final Integer count = this.jdbcTemplate.queryForObject(checkClientSql, Integer.class, clientId);
610+
if (count == null || count == 0) {
611+
throw new ClientNotFoundException(clientId);
612+
}
613+
final String sql = "SELECT " + "(SELECT COUNT(*) FROM m_loan WHERE client_id = c.id AND loan_status_id = 300) as activeLoans, "
614+
+ "(SELECT COALESCE(SUM(principal_outstanding_derived + interest_outstanding_derived + fee_charges_outstanding_derived + penalty_charges_outstanding_derived), 0) "
615+
+ " FROM m_loan WHERE client_id = c.id AND loan_status_id = 300) as totalOutstanding "
616+
+ "FROM m_client c WHERE c.id = ?";
617+
618+
return this.jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
619+
final Integer activeLoans = rs.getInt("activeLoans");
620+
BigDecimal totalOutstanding = rs.getBigDecimal("totalOutstanding");
621+
return ClientPerformanceData.instance(activeLoans, totalOutstanding);
622+
}, clientId);
623+
} catch (EmptyResultDataAccessException e) {
624+
throw new ClientNotFoundException(clientId);
625+
}
626+
}
627+
604628
private static final class ClientToDataMapper implements RowMapper<ClientData> {
605629

606630
private final String schema;
@@ -773,5 +797,4 @@ public ClientData mapRow(final ResultSet rs, final int rowNum) throws SQLExcepti
773797

774798
}
775799
}
776-
777800
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
20+
package org.apache.fineract.integrationtests;
21+
22+
import static org.apache.fineract.integrationtests.common.ClientHelper.createClient;
23+
import static org.apache.fineract.integrationtests.common.Utils.initializeRESTAssured;
24+
import static org.apache.fineract.integrationtests.common.Utils.performServerGet;
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
27+
import io.restassured.builder.RequestSpecBuilder;
28+
import io.restassured.builder.ResponseSpecBuilder;
29+
import io.restassured.http.ContentType;
30+
import io.restassured.specification.RequestSpecification;
31+
import io.restassured.specification.ResponseSpecification;
32+
import java.util.HashMap;
33+
import org.apache.fineract.integrationtests.common.ClientHelper;
34+
import org.apache.fineract.integrationtests.common.Utils;
35+
import org.junit.jupiter.api.BeforeEach;
36+
import org.junit.jupiter.api.Test;
37+
38+
public class ClientPerformanceApiIntegrationTest {
39+
40+
private RequestSpecification requestSpec;
41+
private ResponseSpecification responseSpec;
42+
43+
@BeforeEach
44+
public void setup() {
45+
initializeRESTAssured();
46+
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
47+
this.requestSpec.header("Authorization", "Basic bWlmb3M6cGFzc3dvcmQ=");
48+
this.requestSpec.header("fineract-platform-tenantid", "default");
49+
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
50+
}
51+
52+
@Test
53+
public void testClientPerformanceMetrics() {
54+
// Step 1: Create a Client
55+
final Integer clientId = createClient(this.requestSpec, this.responseSpec);
56+
57+
// Step 2: Use the exact path that worked in Swagger
58+
HashMap<String, Object> performanceData = performServerGet(
59+
this.requestSpec,
60+
this.responseSpec,
61+
"/fineract-provider/api/v1/clients/" + clientId + "/performance",
62+
""
63+
);
64+
65+
assertEquals(0, performanceData.get("activeLoans"), "Should have 0 active loans");
66+
assertEquals(0, ((Number) performanceData.get("totalOutstandingBalance")).intValue(), "Should have 0 balance");
67+
}
68+
}

0 commit comments

Comments
 (0)