feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API

- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview
- Hide Financial & Classification section for non-individual entities in personal data and preview
- Fix legal form column in clients list — fetch legalForm from client API for correct display
- Add person/business icon before client name in list rows
- Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API
- Add registration number format hint (mat-hint) based on constitution type
- Add spacing between verification row and constitution type dropdowns
- Reduce table cell padding and prevent action buttons from being clipped
- Add missing MPL-2.0 license headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:10:51 +05:30
co-authored by Claude Opus 4.6
parent ab01111424
commit b890d24bb7
522 changed files with 51991 additions and 21885 deletions
+169
View File
@@ -0,0 +1,169 @@
/**
* CBS Nexora — India Address Domain Object
* Package: org.apache.fineract.portfolio.client.domain
*/
package org.apache.fineract.portfolio.client.domain;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_client_address_india", uniqueConstraints = {
@UniqueConstraint(columnNames = { "client_id", "address_type" })
})
public class ClientAddressIndia {
public enum AddressType {
PERMANENT, CURRENT, OFFICE, CORRESPONDENCE
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Enumerated(EnumType.STRING)
@Column(name = "address_type", nullable = false, length = 20)
private AddressType addressType;
@Column(name = "address_line_1", length = 500)
private String addressLine1;
@Column(name = "address_line_2", length = 500)
private String addressLine2;
@Column(name = "pin_code", length = 6)
private String pinCode;
@Column(name = "locality", length = 200)
private String locality;
@Column(name = "city", length = 200)
private String city;
@Column(name = "district", length = 200)
private String district;
@Column(name = "state", length = 200)
private String state;
@Column(name = "country", length = 100)
private String country = "India";
@Column(name = "landmark", length = 300)
private String landmark;
// Permanent-specific
@Column(name = "proof_type", length = 100)
private String proofType;
@Column(name = "proof_doc_number", length = 100)
private String proofDocNumber;
// Current-specific
@Column(name = "residence_type", length = 50)
private String residenceType;
@Column(name = "years_at_address")
private Integer yearsAtAddress;
@Column(name = "same_as_permanent")
private Boolean sameAsPermanent = false;
// Office-specific
@Column(name = "org_name", length = 300)
private String orgName;
@Column(name = "department", length = 200)
private String department;
@Column(name = "office_phone", length = 20)
private String officePhone;
@Column(name = "employment_type", length = 100)
private String employmentType;
@Column(name = "employee_id", length = 50)
private String employeeId;
@Column(name = "years_with_org")
private Integer yearsWithOrg;
// Correspondence-specific
@Column(name = "comm_mode", length = 50)
private String commMode;
@Column(name = "contact_number", length = 20)
private String contactNumber;
// Audit
@Column(name = "created_date", updatable = false)
private LocalDateTime createdDate = LocalDateTime.now();
@Column(name = "last_modified_date")
private LocalDateTime lastModifiedDate = LocalDateTime.now();
protected ClientAddressIndia() {}
public static ClientAddressIndia create(Client client, AddressType type) {
ClientAddressIndia addr = new ClientAddressIndia();
addr.client = client;
addr.addressType = type;
return addr;
}
public void update(java.util.Map<String, Object> data) {
if (data.containsKey("addressLine1")) this.addressLine1 = (String) data.get("addressLine1");
if (data.containsKey("addressLine2")) this.addressLine2 = (String) data.get("addressLine2");
if (data.containsKey("pinCode")) this.pinCode = (String) data.get("pinCode");
if (data.containsKey("locality")) this.locality = (String) data.get("locality");
if (data.containsKey("city")) this.city = (String) data.get("city");
if (data.containsKey("district")) this.district = (String) data.get("district");
if (data.containsKey("state")) this.state = (String) data.get("state");
if (data.containsKey("country")) this.country = (String) data.get("country");
if (data.containsKey("landmark")) this.landmark = (String) data.get("landmark");
if (data.containsKey("proofType")) this.proofType = (String) data.get("proofType");
if (data.containsKey("proofDocNumber")) this.proofDocNumber = (String) data.get("proofDocNumber");
if (data.containsKey("residenceType")) this.residenceType = (String) data.get("residenceType");
if (data.containsKey("yearsAtAddress")) this.yearsAtAddress = toInt(data.get("yearsAtAddress"));
if (data.containsKey("sameAsPermanent")) this.sameAsPermanent = (Boolean) data.get("sameAsPermanent");
if (data.containsKey("orgName")) this.orgName = (String) data.get("orgName");
if (data.containsKey("department")) this.department = (String) data.get("department");
if (data.containsKey("officePhone")) this.officePhone = (String) data.get("officePhone");
if (data.containsKey("employmentType")) this.employmentType = (String) data.get("employmentType");
if (data.containsKey("employeeId")) this.employeeId = (String) data.get("employeeId");
if (data.containsKey("yearsWithOrg")) this.yearsWithOrg = toInt(data.get("yearsWithOrg"));
if (data.containsKey("commMode")) this.commMode = (String) data.get("commMode");
if (data.containsKey("contactNumber")) this.contactNumber = (String) data.get("contactNumber");
this.lastModifiedDate = LocalDateTime.now();
}
private Integer toInt(Object val) {
if (val == null) return null;
if (val instanceof Integer) return (Integer) val;
return Integer.parseInt(val.toString());
}
// Getters
public Long getId() { return id; }
public Client getClient() { return client; }
public AddressType getAddressType() { return addressType; }
public String getAddressLine1() { return addressLine1; }
public String getAddressLine2() { return addressLine2; }
public String getPinCode() { return pinCode; }
public String getLocality() { return locality; }
public String getCity() { return city; }
public String getDistrict() { return district; }
public String getState() { return state; }
public String getCountry() { return country; }
public String getLandmark() { return landmark; }
public String getProofType() { return proofType; }
public String getResidenceType() { return residenceType; }
public Boolean getSameAsPermanent() { return sameAsPermanent; }
public String getOrgName() { return orgName; }
public String getEmploymentType() { return employmentType; }
public String getCommMode() { return commMode; }
}
@@ -0,0 +1,219 @@
/**
* CBS Nexora — India Address REST Controller
* Package: org.apache.fineract.portfolio.client.api
*
* Endpoints:
* POST /clients/{clientId}/india-address — Save all 4 address types
* GET /clients/{clientId}/india-address — Get all addresses
* GET /clients/{clientId}/india-address/{type} — Get specific type
* PUT /clients/{clientId}/india-address/{type} — Update specific type
* DELETE /clients/{clientId}/india-address — Delete all addresses
*/
package org.apache.fineract.portfolio.client.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.client.domain.*;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.*;
import java.util.stream.Collectors;
@Path("/v1/clients/{clientId}/india-address")
@Component
@Tag(name = "Client India Address", description = "India-specific multi-type address management")
@RequiredArgsConstructor
public class ClientAddressIndiaApiResource {
private final ClientRepositoryWrapper clientRepository;
private final ClientAddressIndiaRepository addressRepository;
private final FromJsonHelper fromJsonHelper;
/**
* POST /clients/{clientId}/india-address
* Body: { "permanent": {...}, "current": {...}, "office": {...}, "correspondence": {...} }
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "Save all address types", description = "Create or update Permanent, Current, Office, and Correspondence addresses")
public Response saveAllAddresses(
@PathParam("clientId") Long clientId,
String jsonBody) {
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final JsonObject json = fromJsonHelper.parse(jsonBody).getAsJsonObject();
List<Long> savedIds = new ArrayList<>();
for (ClientAddressIndia.AddressType type : ClientAddressIndia.AddressType.values()) {
String key = type.name().toLowerCase();
if (json.has(key) && json.get(key).isJsonObject()) {
JsonObject addrJson = json.getAsJsonObject(key);
Map<String, Object> data = toMap(addrJson);
Optional<ClientAddressIndia> existing = addressRepository.findByClientIdAndAddressType(clientId, type);
ClientAddressIndia addr;
if (existing.isPresent()) {
addr = existing.get();
addr.update(data);
} else {
addr = ClientAddressIndia.create(client, type);
addr.update(data);
}
addressRepository.saveAndFlush(addr);
savedIds.add(addr.getId());
}
}
return Response.ok(Map.of(
"clientId", clientId,
"savedAddressIds", savedIds
)).build();
}
/**
* GET /clients/{clientId}/india-address
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Get all addresses", description = "Retrieve all 4 address types for a client")
public Response getAllAddresses(@PathParam("clientId") Long clientId) {
clientRepository.findOneWithNotFoundDetection(clientId);
List<ClientAddressIndia> addresses = addressRepository.findByClientId(clientId);
Map<String, Object> result = new HashMap<>();
result.put("clientId", clientId);
for (ClientAddressIndia addr : addresses) {
result.put(addr.getAddressType().name().toLowerCase(), toResponseMap(addr));
}
return Response.ok(result).build();
}
/**
* GET /clients/{clientId}/india-address/{type}
*/
@GET
@Path("/{type}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Get specific address type")
public Response getAddress(
@PathParam("clientId") Long clientId,
@PathParam("type") String type) {
clientRepository.findOneWithNotFoundDetection(clientId);
ClientAddressIndia.AddressType addrType = ClientAddressIndia.AddressType.valueOf(type.toUpperCase());
Optional<ClientAddressIndia> addr = addressRepository.findByClientIdAndAddressType(clientId, addrType);
if (addr.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "No " + type + " address found for client " + clientId))
.build();
}
return Response.ok(toResponseMap(addr.get())).build();
}
/**
* PUT /clients/{clientId}/india-address/{type}
*/
@PUT
@Path("/{type}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "Update specific address type")
public Response updateAddress(
@PathParam("clientId") Long clientId,
@PathParam("type") String type,
String jsonBody) {
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
ClientAddressIndia.AddressType addrType = ClientAddressIndia.AddressType.valueOf(type.toUpperCase());
final JsonObject json = fromJsonHelper.parse(jsonBody).getAsJsonObject();
final Map<String, Object> data = toMap(json);
Optional<ClientAddressIndia> existing = addressRepository.findByClientIdAndAddressType(clientId, addrType);
ClientAddressIndia addr;
if (existing.isPresent()) {
addr = existing.get();
addr.update(data);
} else {
addr = ClientAddressIndia.create(client, addrType);
addr.update(data);
}
addressRepository.saveAndFlush(addr);
return Response.ok(Map.of(
"resourceId", addr.getId(),
"clientId", clientId,
"addressType", type.toUpperCase()
)).build();
}
/**
* DELETE /clients/{clientId}/india-address
*/
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "Delete all addresses")
public Response deleteAllAddresses(@PathParam("clientId") Long clientId) {
clientRepository.findOneWithNotFoundDetection(clientId);
addressRepository.deleteByClientId(clientId);
return Response.ok(Map.of("clientId", clientId, "deleted", true)).build();
}
private Map<String, Object> toResponseMap(ClientAddressIndia addr) {
Map<String, Object> map = new HashMap<>();
map.put("id", addr.getId());
map.put("addressType", addr.getAddressType().name());
map.put("addressLine1", addr.getAddressLine1());
map.put("addressLine2", addr.getAddressLine2());
map.put("pinCode", addr.getPinCode());
map.put("locality", addr.getLocality());
map.put("city", addr.getCity());
map.put("district", addr.getDistrict());
map.put("state", addr.getState());
map.put("country", addr.getCountry());
map.put("landmark", addr.getLandmark());
map.put("proofType", addr.getProofType());
map.put("residenceType", addr.getResidenceType());
map.put("sameAsPermanent", addr.getSameAsPermanent());
map.put("orgName", addr.getOrgName());
map.put("employmentType", addr.getEmploymentType());
map.put("commMode", addr.getCommMode());
return map;
}
private Map<String, Object> toMap(JsonObject json) {
Map<String, Object> map = new HashMap<>();
json.entrySet().forEach(e -> {
if (e.getValue().isJsonNull()) {
map.put(e.getKey(), null);
} else if (e.getValue().isJsonPrimitive()) {
var prim = e.getValue().getAsJsonPrimitive();
if (prim.isBoolean()) map.put(e.getKey(), prim.getAsBoolean());
else if (prim.isNumber()) map.put(e.getKey(), prim.getAsInt());
else map.put(e.getKey(), prim.getAsString());
}
});
return map;
}
}
@@ -0,0 +1,20 @@
/**
* CBS Nexora — India Address Repository
* Package: org.apache.fineract.portfolio.client.domain
*/
package org.apache.fineract.portfolio.client.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface ClientAddressIndiaRepository extends JpaRepository<ClientAddressIndia, Long> {
List<ClientAddressIndia> findByClientId(Long clientId);
Optional<ClientAddressIndia> findByClientIdAndAddressType(Long clientId, ClientAddressIndia.AddressType addressType);
void deleteByClientId(Long clientId);
boolean existsByClientId(Long clientId);
}
+173
View File
@@ -0,0 +1,173 @@
/**
* CBS Nexora — Entity KYC Domain Object
* Package: org.apache.fineract.portfolio.client.domain
*/
package org.apache.fineract.portfolio.client.domain;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "m_client_entity_kyc", uniqueConstraints = {
@UniqueConstraint(columnNames = { "client_id" })
})
public class ClientEntityKyc {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id", nullable = false)
private Client client;
@Column(name = "constitution_type", length = 50)
private String constitutionType;
// ── MSME ──
@Column(name = "is_msme")
private Boolean isMsme = false;
@Column(name = "udyam_number", length = 19)
private String udyamNumber;
@Column(name = "enterprise_name", length = 300)
private String enterpriseName;
@Column(name = "business_sector", length = 50)
private String businessSector;
@Column(name = "msme_turnover", precision = 18, scale = 2)
private BigDecimal msmeTurnover;
@Column(name = "msme_investment", precision = 18, scale = 2)
private BigDecimal msmeInvestment;
@Column(name = "msme_type", length = 20)
private String msmeType;
// ── Entity KYC Document ──
@Column(name = "entity_kyc_doc_type", length = 100)
private String entityKycDocType;
@Column(name = "entity_kyc_doc_number", length = 100)
private String entityKycDocNumber;
@Column(name = "entity_kyc_doc_issue_date")
private LocalDate entityKycDocIssueDate;
@Column(name = "entity_kyc_doc_expiry_date")
private LocalDate entityKycDocExpiryDate;
// ── Authorized Signatory ──
@Column(name = "signatory_name", length = 200)
private String signatoryName;
@Column(name = "signatory_designation", length = 100)
private String signatoryDesignation;
@Column(name = "signatory_pan", length = 10)
private String signatoryPan;
@Column(name = "signatory_aadhaar_masked", length = 12)
private String signatoryAadhaarMasked;
@Column(name = "signatory_kyc_doc_type", length = 100)
private String signatoryKycDocType;
@Column(name = "signatory_kyc_doc_number", length = 100)
private String signatoryKycDocNumber;
// ── Authorization Document ──
@Column(name = "authorization_doc_type", length = 100)
private String authorizationDocType;
@Column(name = "authorization_doc_number", length = 100)
private String authorizationDocNumber;
@Column(name = "authorization_date")
private LocalDate authorizationDate;
// ── Risk ──
@Column(name = "entity_risk_category")
private Integer entityRiskCategory = 1;
@Column(name = "entity_remarks", columnDefinition = "TEXT")
private String entityRemarks;
// ── Audit ──
@Column(name = "created_date", updatable = false)
private LocalDateTime createdDate = LocalDateTime.now();
@Column(name = "last_modified_date")
private LocalDateTime lastModifiedDate = LocalDateTime.now();
protected ClientEntityKyc() {}
public static ClientEntityKyc create(Client client, String constitutionType) {
ClientEntityKyc kyc = new ClientEntityKyc();
kyc.client = client;
kyc.constitutionType = constitutionType;
return kyc;
}
public void update(java.util.Map<String, Object> data) {
if (data.containsKey("constitutionType")) this.constitutionType = (String) data.get("constitutionType");
if (data.containsKey("isMsme")) this.isMsme = (Boolean) data.get("isMsme");
if (data.containsKey("udyamNumber")) this.udyamNumber = (String) data.get("udyamNumber");
if (data.containsKey("enterpriseName")) this.enterpriseName = (String) data.get("enterpriseName");
if (data.containsKey("businessSector")) this.businessSector = (String) data.get("businessSector");
if (data.containsKey("msmeTurnover")) this.msmeTurnover = toBigDecimal(data.get("msmeTurnover"));
if (data.containsKey("msmeInvestment")) this.msmeInvestment = toBigDecimal(data.get("msmeInvestment"));
if (data.containsKey("msmeType")) this.msmeType = (String) data.get("msmeType");
if (data.containsKey("entityKycDocType")) this.entityKycDocType = (String) data.get("entityKycDocType");
if (data.containsKey("entityKycDocNumber")) this.entityKycDocNumber = (String) data.get("entityKycDocNumber");
if (data.containsKey("signatoryName")) this.signatoryName = (String) data.get("signatoryName");
if (data.containsKey("signatoryDesignation")) this.signatoryDesignation = (String) data.get("signatoryDesignation");
if (data.containsKey("signatoryPan")) this.signatoryPan = (String) data.get("signatoryPan");
if (data.containsKey("signatoryAadhaar")) {
String raw = (String) data.get("signatoryAadhaar");
this.signatoryAadhaarMasked = raw != null && raw.length() == 12 ? "XXXXXXXX" + raw.substring(8) : raw;
}
if (data.containsKey("signatoryKycDocType")) this.signatoryKycDocType = (String) data.get("signatoryKycDocType");
if (data.containsKey("signatoryKycDocNumber")) this.signatoryKycDocNumber = (String) data.get("signatoryKycDocNumber");
if (data.containsKey("authorizationDocType")) this.authorizationDocType = (String) data.get("authorizationDocType");
if (data.containsKey("authorizationDocNumber")) this.authorizationDocNumber = (String) data.get("authorizationDocNumber");
if (data.containsKey("entityRiskCategory")) this.entityRiskCategory = (Integer) data.get("entityRiskCategory");
if (data.containsKey("entityRemarks")) this.entityRemarks = (String) data.get("entityRemarks");
this.lastModifiedDate = LocalDateTime.now();
}
private BigDecimal toBigDecimal(Object val) {
if (val == null) return null;
if (val instanceof BigDecimal) return (BigDecimal) val;
return new BigDecimal(val.toString());
}
// Getters
public Long getId() { return id; }
public Client getClient() { return client; }
public String getConstitutionType() { return constitutionType; }
public Boolean getIsMsme() { return isMsme; }
public String getUdyamNumber() { return udyamNumber; }
public String getEnterpriseName() { return enterpriseName; }
public String getBusinessSector() { return businessSector; }
public BigDecimal getMsmeTurnover() { return msmeTurnover; }
public BigDecimal getMsmeInvestment() { return msmeInvestment; }
public String getMsmeType() { return msmeType; }
public String getEntityKycDocType() { return entityKycDocType; }
public String getEntityKycDocNumber() { return entityKycDocNumber; }
public LocalDate getEntityKycDocIssueDate() { return entityKycDocIssueDate; }
public LocalDate getEntityKycDocExpiryDate() { return entityKycDocExpiryDate; }
public String getSignatoryName() { return signatoryName; }
public String getSignatoryDesignation() { return signatoryDesignation; }
public String getSignatoryPan() { return signatoryPan; }
public String getSignatoryAadhaarMasked() { return signatoryAadhaarMasked; }
public String getAuthorizationDocType() { return authorizationDocType; }
public String getAuthorizationDocNumber() { return authorizationDocNumber; }
public LocalDate getAuthorizationDate() { return authorizationDate; }
public Integer getEntityRiskCategory() { return entityRiskCategory; }
public String getEntityRemarks() { return entityRemarks; }
}
@@ -0,0 +1,140 @@
/**
* CBS Nexora — Entity KYC REST Controller
* Package: org.apache.fineract.portfolio.client.api
*
* Endpoints:
* POST /clients/{clientId}/entity-kyc — Create or update entity KYC + MSME
* GET /clients/{clientId}/entity-kyc — Retrieve entity KYC data
* DELETE /clients/{clientId}/entity-kyc — Delete entity KYC data
*/
package org.apache.fineract.portfolio.client.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.client.domain.*;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.google.gson.JsonObject;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Path("/v1/clients/{clientId}/entity-kyc")
@Component
@Tag(name = "Client Entity KYC", description = "Non-Individual client MSME & KYC management")
@RequiredArgsConstructor
public class ClientEntityKycApiResource {
private final ClientRepositoryWrapper clientRepository;
private final ClientEntityKycRepository entityKycRepository;
private final FromJsonHelper fromJsonHelper;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "Create or Update Entity KYC", description = "Save MSME details, entity KYC document, signatory KYC, authorization document, and risk assessment")
public Response saveEntityKyc(
@PathParam("clientId") Long clientId,
String jsonBody) {
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final JsonObject json = fromJsonHelper.parse(jsonBody).getAsJsonObject();
final Map<String, Object> data = toMap(json);
Optional<ClientEntityKyc> existing = entityKycRepository.findByClientId(clientId);
ClientEntityKyc entityKyc;
if (existing.isPresent()) {
entityKyc = existing.get();
entityKyc.update(data);
} else {
String constitutionType = json.has("constitutionType") ? json.get("constitutionType").getAsString() : null;
entityKyc = ClientEntityKyc.create(client, constitutionType);
entityKyc.update(data);
}
entityKycRepository.saveAndFlush(entityKyc);
return Response.ok(Map.of(
"resourceId", entityKyc.getId(),
"clientId", clientId
)).build();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Retrieve Entity KYC", description = "Get MSME, signatory, authorization, and risk data for a non-individual client")
public Response getEntityKyc(@PathParam("clientId") Long clientId) {
clientRepository.findOneWithNotFoundDetection(clientId);
Optional<ClientEntityKyc> entityKyc = entityKycRepository.findByClientId(clientId);
if (entityKyc.isEmpty()) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "No entity KYC found for client " + clientId))
.build();
}
ClientEntityKyc kyc = entityKyc.get();
Map<String, Object> result = new HashMap<>();
result.put("id", kyc.getId());
result.put("clientId", clientId);
result.put("constitutionType", kyc.getConstitutionType());
result.put("isMsme", kyc.getIsMsme());
result.put("udyamNumber", kyc.getUdyamNumber());
result.put("enterpriseName", kyc.getEnterpriseName());
result.put("businessSector", kyc.getBusinessSector());
result.put("msmeTurnover", kyc.getMsmeTurnover());
result.put("msmeInvestment", kyc.getMsmeInvestment());
result.put("msmeType", kyc.getMsmeType());
result.put("entityKycDocType", kyc.getEntityKycDocType());
result.put("entityKycDocNumber", kyc.getEntityKycDocNumber());
result.put("entityKycDocIssueDate", kyc.getEntityKycDocIssueDate());
result.put("entityKycDocExpiryDate", kyc.getEntityKycDocExpiryDate());
result.put("signatoryName", kyc.getSignatoryName());
result.put("signatoryDesignation", kyc.getSignatoryDesignation());
result.put("signatoryPan", kyc.getSignatoryPan());
result.put("signatoryAadhaarMasked", kyc.getSignatoryAadhaarMasked());
result.put("authorizationDocType", kyc.getAuthorizationDocType());
result.put("authorizationDocNumber", kyc.getAuthorizationDocNumber());
result.put("authorizationDate", kyc.getAuthorizationDate());
result.put("entityRiskCategory", kyc.getEntityRiskCategory());
result.put("entityRemarks", kyc.getEntityRemarks());
return Response.ok(result).build();
}
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Transactional
@Operation(summary = "Delete Entity KYC")
public Response deleteEntityKyc(@PathParam("clientId") Long clientId) {
clientRepository.findOneWithNotFoundDetection(clientId);
entityKycRepository.deleteByClientId(clientId);
return Response.ok(Map.of("clientId", clientId, "deleted", true)).build();
}
private Map<String, Object> toMap(JsonObject json) {
Map<String, Object> map = new HashMap<>();
json.entrySet().forEach(e -> {
if (e.getValue().isJsonNull()) {
map.put(e.getKey(), null);
} else if (e.getValue().isJsonPrimitive()) {
var prim = e.getValue().getAsJsonPrimitive();
if (prim.isBoolean()) map.put(e.getKey(), prim.getAsBoolean());
else if (prim.isNumber()) {
if (prim.getAsString().contains(".")) map.put(e.getKey(), prim.getAsBigDecimal());
else map.put(e.getKey(), prim.getAsInt());
}
else map.put(e.getKey(), prim.getAsString());
}
});
return map;
}
}
@@ -0,0 +1,17 @@
/**
* CBS Nexora — Entity KYC Repository
* Package: org.apache.fineract.portfolio.client.domain
*/
package org.apache.fineract.portfolio.client.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ClientEntityKycRepository extends JpaRepository<ClientEntityKyc, Long> {
Optional<ClientEntityKyc> findByClientId(Long clientId);
boolean existsByClientId(Long clientId);
void deleteByClientId(Long clientId);
}
+75
View File
@@ -0,0 +1,75 @@
# CBS Nexora — Backend Schema & API Changes
## Files in this directory
| File | Purpose |
|------|---------|
| `V1__client_schema_extensions.sql` | DB migration — run on Fineract MySQL/MariaDB |
| `ClientEntityKyc.java` | JPA Entity for `m_client_entity_kyc` table |
| `ClientAddressIndia.java` | JPA Entity for `m_client_address_india` table |
| `ClientEntityKycRepository.java` | Spring Data repository |
| `ClientAddressIndiaRepository.java` | Spring Data repository |
| `ClientEntityKycApiResource.java` | REST controller — `/clients/{id}/entity-kyc` |
| `ClientAddressIndiaApiResource.java` | REST controller — `/clients/{id}/india-address` |
## How to apply
### Step 1: Run SQL Migration
```bash
mysql -u root -p fineract_tenantdb < V1__client_schema_extensions.sql
```
### Step 2: Copy Java files to Fineract source
```
ClientEntityKyc.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/
ClientAddressIndia.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/
ClientEntityKycRepository.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/
ClientAddressIndiaRepository.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/
ClientEntityKycApiResource.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/
ClientAddressIndiaApiResource.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/
```
### Step 3: Update existing Fineract Client API
In `ClientApiResource.java`, the `POST /clients` endpoint needs to accept these additional fields in the request body:
- `nationality`, `annualTurnover`, `creditLimit`
- `isMinor`, `ageYears`, `ageMonths`, `ageDays`
- `guardianName`, `guardianRelation`, `guardianMobile`, `guardianIdNumber`, `guardianAddress`
In `ClientWritePlatformServiceJpaRepositoryImpl.java`, map these fields to the `m_client` entity during create/update.
### Step 4: Register new API resources
Add to `fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractWebMvcConfigurer.java` or equivalent Spring config.
### Step 5: Rebuild Fineract
```bash
./gradlew bootRun
```
## API Reference
### Entity KYC
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/clients/{clientId}/entity-kyc` | Create/update entity KYC + MSME |
| GET | `/clients/{clientId}/entity-kyc` | Retrieve entity KYC |
| DELETE | `/clients/{clientId}/entity-kyc` | Delete entity KYC |
### India Address
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/clients/{clientId}/india-address` | Save all 4 address types |
| GET | `/clients/{clientId}/india-address` | Get all addresses |
| GET | `/clients/{clientId}/india-address/{type}` | Get specific type (PERMANENT/CURRENT/OFFICE/CORRESPONDENCE) |
| PUT | `/clients/{clientId}/india-address/{type}` | Update specific type |
| DELETE | `/clients/{clientId}/india-address` | Delete all addresses |
## Tables Summary
| Table | New/Modified | Records |
|-------|-------------|---------|
| `m_client` | Modified — 12 new columns | Guardian, nationality, minor, turnover |
| `m_client_non_person` | Modified — 13 new columns | MSME, CIN, enterprise PAN, signatory |
| `m_client_kyc` | Modified — 19 new columns | AML, freeze, form60/61, form15G/H, KYC doc |
| `m_client_entity_kyc` | **New** | Entity-specific KYC, MSME, signatory, auth doc |
| `m_client_address_india` | **New** | 4-type India address with PIN lookup fields |
| `m_code_value` | Modified | Added Gender: Transgender, Other |
@@ -0,0 +1,149 @@
-- ============================================================
-- CBS Nexora — Client Schema Extensions
-- Fineract DB Migration Script
-- Run this AFTER backing up your database
-- ============================================================
-- ─── 1. m_client — New columns ───
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS nationality VARCHAR(20) DEFAULT 'INDIAN';
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS annual_turnover DECIMAL(18,2);
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS credit_limit DECIMAL(18,2);
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS is_minor TINYINT(1) DEFAULT 0;
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS age_years INT;
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS age_months INT;
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS age_days INT;
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS guardian_name VARCHAR(200);
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS guardian_relation VARCHAR(50);
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS guardian_mobile VARCHAR(15);
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS guardian_id_number VARCHAR(50);
ALTER TABLE m_client ADD COLUMN IF NOT EXISTS guardian_address TEXT;
-- ─── 2. m_client_non_person — New columns ───
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS date_of_registration DATE;
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS enterprise_pan VARCHAR(10);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS cin_number VARCHAR(21);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS authorized_signatory VARCHAR(200);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS additional_info VARCHAR(500);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS credit_limit DECIMAL(18,2);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS is_msme TINYINT(1) DEFAULT 0;
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS udyam_number VARCHAR(19);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS enterprise_name VARCHAR(300);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS business_sector VARCHAR(50);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS msme_turnover DECIMAL(18,2);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS msme_investment DECIMAL(18,2);
ALTER TABLE m_client_non_person ADD COLUMN IF NOT EXISTS msme_type VARCHAR(20);
-- ─── 3. m_client_kyc — New columns ───
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS has_pan TINYINT(1) DEFAULT 1;
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS pan_aadhaar_link_status VARCHAR(20);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS kyc_document_name VARCHAR(100);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS kyc_document_number VARCHAR(100);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS kyc_doc_issue_date DATE;
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS kyc_doc_expiry_date DATE;
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS kyc_expiry_date DATE;
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS aml_check_status VARCHAR(20) DEFAULT 'CLEAR';
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS banned_entity_match VARCHAR(20) DEFAULT 'NO';
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS willful_defaulter VARCHAR(5) DEFAULT 'NO';
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS ecgc_sal_flag VARCHAR(5) DEFAULT 'NO';
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS dormant_flag VARCHAR(20) DEFAULT 'ACTIVE';
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS account_status VARCHAR(20) DEFAULT 'ACTIVE';
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS freeze_reason VARCHAR(50);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS freeze_authority VARCHAR(50);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS form60_type VARCHAR(10);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS form60_date DATE;
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS form15_applicable TINYINT(1) DEFAULT 0;
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS form15_type VARCHAR(5);
ALTER TABLE m_client_kyc ADD COLUMN IF NOT EXISTS form15_fy VARCHAR(10);
-- ─── 4. m_client_entity_kyc — New table ───
CREATE TABLE IF NOT EXISTS m_client_entity_kyc (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id BIGINT NOT NULL,
constitution_type VARCHAR(50),
-- MSME
is_msme TINYINT(1) DEFAULT 0,
udyam_number VARCHAR(19),
enterprise_name VARCHAR(300),
business_sector VARCHAR(50),
msme_turnover DECIMAL(18,2),
msme_investment DECIMAL(18,2),
msme_type VARCHAR(20),
-- Entity KYC Document
entity_kyc_doc_type VARCHAR(100),
entity_kyc_doc_number VARCHAR(100),
entity_kyc_doc_issue_date DATE,
entity_kyc_doc_expiry_date DATE,
-- Authorized Signatory
signatory_name VARCHAR(200),
signatory_designation VARCHAR(100),
signatory_pan VARCHAR(10),
signatory_aadhaar_masked VARCHAR(12),
signatory_kyc_doc_type VARCHAR(100),
signatory_kyc_doc_number VARCHAR(100),
-- Authorization Document
authorization_doc_type VARCHAR(100),
authorization_doc_number VARCHAR(100),
authorization_date DATE,
-- Risk
entity_risk_category INT DEFAULT 1,
entity_remarks TEXT,
-- Audit
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
last_modified_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES m_client(id) ON DELETE CASCADE,
UNIQUE KEY uk_client_entity_kyc (client_id)
);
-- ─── 5. m_client_address_india — New table ───
CREATE TABLE IF NOT EXISTS m_client_address_india (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id BIGINT NOT NULL,
address_type ENUM('PERMANENT','CURRENT','OFFICE','CORRESPONDENCE') NOT NULL,
address_line_1 VARCHAR(500),
address_line_2 VARCHAR(500),
pin_code VARCHAR(6),
locality VARCHAR(200),
city VARCHAR(200),
district VARCHAR(200),
state VARCHAR(200),
country VARCHAR(100) DEFAULT 'India',
landmark VARCHAR(300),
-- Permanent-specific
proof_type VARCHAR(100),
proof_doc_number VARCHAR(100),
-- Current-specific
residence_type VARCHAR(50),
years_at_address INT,
same_as_permanent TINYINT(1) DEFAULT 0,
-- Office-specific
org_name VARCHAR(300),
department VARCHAR(200),
office_phone VARCHAR(20),
employment_type VARCHAR(100),
employee_id VARCHAR(50),
years_with_org INT,
-- Correspondence-specific
comm_mode VARCHAR(50),
contact_number VARCHAR(20),
-- Audit
created_date DATETIME DEFAULT CURRENT_TIMESTAMP,
last_modified_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES m_client(id) ON DELETE CASCADE,
UNIQUE KEY uk_client_address_type (client_id, address_type)
);
-- ─── 6. Constitution Type code values (if not using API-driven options) ───
-- Find Gender code and add missing values
SET @gender_code_id = (SELECT id FROM m_code WHERE code_name = 'Gender' LIMIT 1);
INSERT IGNORE INTO m_code_value (code_id, code_value, order_position, is_active)
VALUES
(@gender_code_id, 'Transgender', 3, 1),
(@gender_code_id, 'Other', 4, 1);
-- ─── 7. Indexes for performance ───
CREATE INDEX IF NOT EXISTS idx_client_entity_kyc_client ON m_client_entity_kyc(client_id);
CREATE INDEX IF NOT EXISTS idx_client_address_india_client ON m_client_address_india(client_id);
CREATE INDEX IF NOT EXISTS idx_client_address_india_type ON m_client_address_india(client_id, address_type);
CREATE INDEX IF NOT EXISTS idx_client_kyc_expiry ON m_client_kyc(kyc_expiry_date);
CREATE INDEX IF NOT EXISTS idx_client_kyc_aml ON m_client_kyc(aml_check_status);
CREATE INDEX IF NOT EXISTS idx_client_minor ON m_client(is_minor);