Add extended fields to ClientNonPersonDetails: authorizedSignatory, additionalInfo, dateOfRegistration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:06:45 +05:30
co-authored by Claude Opus 4.6
parent 90c21d40c1
commit 87e132f93d
147 changed files with 422232 additions and 167 deletions
@@ -19,6 +19,7 @@
package org.apache.fineract.infrastructure.core.service;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData;
import org.apache.fineract.infrastructure.configuration.service.ExternalServicesPropertiesReadPlatformService;
import org.apache.fineract.infrastructure.core.domain.EmailDetail;
@@ -27,6 +28,7 @@ import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class GmailBackedPlatformEmailService implements PlatformEmailService {
@@ -90,7 +92,7 @@ public class GmailBackedPlatformEmailService implements PlatformEmailService {
mailSender.send(message);
} catch (Exception e) {
throw new PlatformEmailSendException(e);
log.warn("Email sending failed for address [{}]. Skipping. Error: {}", emailDetails.getAddress(), e.getMessage());
}
}
}
@@ -22,6 +22,7 @@ import com.google.gson.JsonObject;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
@@ -33,6 +34,7 @@ import java.util.Set;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientAddress;
@Entity
@@ -47,6 +49,14 @@ public class Address extends AbstractPersistableCustom<Long> {
@OneToMany(mappedBy = "address", cascade = CascadeType.ALL)
private Set<ClientAddress> clientaddress;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "client_id")
private Client client;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "address_type_id")
private CodeValue addressType;
@Column(name = "street")
private String street;
@@ -377,4 +387,20 @@ public class Address extends AbstractPersistableCustom<Long> {
this.updatedOn = updatedOn;
}
public Client getClient() {
return this.client;
}
public void setClient(Client client) {
this.client = client;
}
public CodeValue getAddressType() {
return this.addressType;
}
public void setAddressType(CodeValue addressType) {
this.addressType = addressType;
}
}
@@ -63,6 +63,8 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
final Address address = createAddress(jsonObject);
address.setClient(client);
address.setAddressType(addressTypeIdCodeValue);
addressRepository.save(address);
final ClientAddress clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address);
@@ -86,6 +88,8 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId);
final Address address = createAddress(jsonObject);
address.setClient(client);
address.setAddressType(addressTypeIdCodeValue);
addressRepository.save(address);
clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address);
@@ -0,0 +1,193 @@
/**
* 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.client.api;
import static org.apache.fineract.util.StreamResponseUtil.DISPOSITION_TYPE_ATTACHMENT;
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.command.core.CommandPipeline;
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorContext;
import org.apache.fineract.infrastructure.contentstore.detector.ContentDetectorManager;
import org.apache.fineract.infrastructure.documentmanagement.api.FileUploadValidator;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCreateCommand;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentDeleteCommand;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateRequest;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentCreateResponse;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentData;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteRequest;
import org.apache.fineract.infrastructure.documentmanagement.data.DocumentDeleteResponse;
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentReadPlatformService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.util.StreamResponseUtil;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.springframework.stereotype.Component;
/**
* REST API for managing documents attached to family members.
* Delegates to Fineract's generic document infrastructure (m_document table)
* using entityType = "family_members".
*/
@Path("/v1/clients/{clientId}/familymembers/{familyMemberId}/documents")
@Component
@Tag(name = "Client Family Member Documents", description = "Upload, list, download and delete documents for family members (e.g. marriage certificates)")
@RequiredArgsConstructor
public class ClientFamilyMemberDocumentApiResource {
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "FAMILYMEMBERDOCUMENT";
private static final String ENTITY_TYPE = "family_members";
private final PlatformSecurityContext context;
private final DocumentReadPlatformService documentReadPlatformService;
private final FileUploadValidator fileUploadValidator;
private final ContentDetectorManager contentDetectorManager;
private final CommandPipeline commandPipeline;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List all documents for a family member")
public List<DocumentData> retrieveAllDocuments(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return documentReadPlatformService.retrieveAllDocuments(ENTITY_TYPE, familyMemberId);
}
@GET
@Path("/{documentId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a specific document for a family member")
public DocumentData getDocument(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
return documentReadPlatformService.retrieveDocument(ENTITY_TYPE, familyMemberId, documentId);
}
@GET
@Path("/{documentId}/attachment")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_OCTET_STREAM })
@Operation(summary = "Download the file associated with a family member document")
public Response downloadFile(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final var content = documentReadPlatformService.retrieveDocumentContent(ENTITY_TYPE, familyMemberId, documentId);
return StreamResponseUtil
.ok(StreamResponseUtil.StreamResponseData.builder().type(content.getContentType()).fileName(content.getFileName())
.dispositionType(DISPOSITION_TYPE_ATTACHMENT).stream(content.getStream()).size(content.getSize()).build());
}
@POST
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Upload a document for a family member", description = """
Upload a document (e.g. marriage certificate) for a family member using multipart form data.
Body parts:
- name: name or summary of the document
- description: description of the document
- file: the file to be uploaded
""")
public DocumentCreateResponse createDocument(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@HeaderParam(CONTENT_LENGTH) final Long fileSize,
@FormDataParam("file") final InputStream is,
@FormDataParam("file") final FormDataContentDisposition fileDetails,
@FormDataParam("file") final FormDataBodyPart filePart,
@FormDataParam("name") final String name,
@FormDataParam("description") final String description) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME_FOR_PERMISSIONS);
fileUploadValidator.validate(fileSize, is, fileDetails, filePart);
final var command = new DocumentCreateCommand();
var type = Optional.ofNullable(filePart.getMediaType()).map(MediaType::toString)
.or(() -> Optional.of(contentDetectorManager
.detect(ContentDetectorContext.builder().fileName(fileDetails.getFileName()).build()).getMimeType()))
.orElse(APPLICATION_OCTET_STREAM_VALUE);
command.setPayload(DocumentCreateRequest.builder()
.entityId(familyMemberId)
.entityType(ENTITY_TYPE)
.name(name)
.description(description)
.fileName(fileDetails.getFileName())
.size(fileSize)
.type(type)
.stream(is)
.build());
final Supplier<DocumentCreateResponse> response = commandPipeline.send(command);
return response.get();
}
@DELETE
@Path("/{documentId}")
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a family member document")
public DocumentDeleteResponse deleteDocument(
@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@PathParam("familyMemberId") @Parameter(description = "familyMemberId") final Long familyMemberId,
@PathParam("documentId") @Parameter(description = "documentId") final Long documentId) {
this.context.authenticatedUser().validateHasDeletePermission(RESOURCE_NAME_FOR_PERMISSIONS);
final var command = new DocumentDeleteCommand();
command.setPayload(DocumentDeleteRequest.builder()
.id(documentId)
.entityId(familyMemberId)
.entityType(ENTITY_TYPE)
.build());
final Supplier<DocumentDeleteResponse> response = commandPipeline.send(command);
return response.get();
}
}
@@ -0,0 +1,103 @@
/**
* 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.client.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.data.ClientForm60Data;
import org.apache.fineract.portfolio.client.service.ClientForm60ReadPlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clients/{clientId}/form60")
@Component
@Tag(name = "Client Form 60", description = "Form 60 declaration as per Rule 114B of Income Tax Rules, 1962. "
+ "Filed by individuals who do not have a PAN for specified financial transactions.")
@RequiredArgsConstructor
public class ClientForm60ApiResource {
private static final String RESOURCE_NAME = "CLIENTFORM60";
private final PlatformSecurityContext context;
private final ClientForm60ReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<ClientForm60Data> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<List> listSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Submit Form 60 for a Client",
description = "Creates a new Form 60 declaration for the given client.")
public String createForm60(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.createClientForm60(clientId)
.withJson(apiRequestBodyAsJson)
.build();
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return toApiJsonSerializer.serialize(result);
}
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve all Form 60 declarations for a Client",
description = "Returns all Form 60 records for the given client.")
public String retrieveAllForm60(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final List<ClientForm60Data> data = readPlatformService.retrieveAllByClientId(clientId);
return listSerializer.serialize(data);
}
@GET
@Path("{form60Id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a specific Form 60 by ID",
description = "Returns the Form 60 record identified by {form60Id} for the given client.")
public String retrieveForm60(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@PathParam("form60Id") @Parameter(description = "Form 60 record identifier") final Long form60Id) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final ClientForm60Data data = readPlatformService.retrieveOne(clientId, form60Id);
return toApiJsonSerializer.serialize(data);
}
}
@@ -34,6 +34,10 @@ import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -43,10 +47,12 @@ import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSer
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.data.ClientKycApiConstants;
import org.apache.fineract.portfolio.client.data.ClientKycData;
import org.apache.fineract.portfolio.client.data.ClientKycRequest;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.service.ClientKycReadPlatformService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.fineract.portfolio.client.data.ClientKycRequest;
import org.apache.fineract.portfolio.client.service.ClientKycReadPlatformService;
import org.springframework.stereotype.Component;
/**
@@ -80,6 +86,7 @@ public class ClientKycApiResource {
private final ClientKycReadPlatformService readPlatformService;
private final DefaultToApiJsonSerializer<ClientKycData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ClientRepositoryWrapper clientRepository;
// ─────────────────────────────────────────────────────────────────────────
// INITIATE KYC – POST /v1/clients/{clientId}/kyc
@@ -103,16 +110,131 @@ public class ClientKycApiResource {
@ApiResponse(responseCode = "409", description = "KYC already exists for this client")
public CommandProcessingResult initiateKyc(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
LOG.info("KYC INITIATE received: clientId={}, requestBody={}", clientId, apiRequestBodyAsJson);
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.initiateClientKyc(clientId)
.withJson(toApiJsonSerializer.serialize(clientKycRequest))
.withJson(apiRequestBodyAsJson)
.build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
// ─────────────────────────────────────────────────────────────────────────
// TEMPLATE – GET /v1/clients/{clientId}/kyc/template
// ─────────────────────────────────────────────────────────────────────────
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve KYC document template for a Client",
description = "Returns the list of required KYC documents based on the client's type "
+ "(clientTypeId code value). Used by the UI to populate the KYC document dropdown.")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveKycTemplate(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final Map<String, Object> template = new LinkedHashMap<>();
template.put("clientId", clientId);
template.put("clientDisplayName", client.getDisplayName());
// Resolve client type label from code value
final String clientTypeName = client.clientType() != null ? client.clientType().getLabel() : null;
final Integer clientTypeId = client.clientType() != null ? client.clientType().getId().intValue() : null;
final Integer legalFormId = client.getLegalForm();
template.put("clientTypeId", clientTypeId);
template.put("clientTypeName", clientTypeName);
template.put("legalFormId", legalFormId);
template.put("requiredDocuments", getRequiredDocuments(clientTypeName, legalFormId));
return toApiJsonSerializer.serialize(template);
}
/**
* Returns required KYC documents based on client type (RBI Master Direction on KYC).
*/
private List<Map<String, Object>> getRequiredDocuments(final String clientTypeName, final Integer legalFormId) {
final List<Map<String, Object>> docs = new ArrayList<>();
// Individual types (legalFormId=1)
if (legalFormId != null && legalFormId == 1) {
docs.add(doc("Aadhaar Card", "AADHAAR", true));
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("Voter ID", "VOTER_ID", false));
docs.add(doc("Driving License", "DRIVING_LICENSE", false));
docs.add(doc("Passport", "PASSPORT", false));
docs.add(doc("NREGA Job Card", "NREGA", false));
return docs;
}
// Non-individual types (legalFormId=2) – based on clientTypeName
if (clientTypeName == null) {
return docs;
}
final String type = clientTypeName.toUpperCase().trim();
if (type.contains("PROPRIETORSHIP")) {
docs.add(doc("PAN Card of Proprietor", "PAN", true));
docs.add(doc("GST Registration Certificate", "GST_CERT", true));
docs.add(doc("Shop & Establishment License", "SHOP_LICENSE", false));
docs.add(doc("Udyam Registration Certificate", "UDYAM", false));
docs.add(doc("Proprietor KYC - Aadhaar", "AADHAAR", true));
} else if (type.contains("PARTNERSHIP") || type.contains("LLP")) {
docs.add(doc("Registration Certificate", "REG_CERT", true));
docs.add(doc("Partnership Deed", "PARTNERSHIP_DEED", true));
docs.add(doc("PAN Card of Firm", "PAN", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("PRIVATE") || type.contains("PUBLIC") || type.contains("CORPORATE")) {
docs.add(doc("Certificate of Incorporation", "COI", true));
docs.add(doc("Memorandum of Association", "MOA", true));
docs.add(doc("Articles of Association", "AOA", true));
docs.add(doc("PAN Card of Company", "PAN", true));
docs.add(doc("Board Resolution", "BOARD_RESOLUTION", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("TRUST")) {
docs.add(doc("Registration Certificate", "REG_CERT", true));
docs.add(doc("Trust Deed", "TRUST_DEED", true));
docs.add(doc("PAN Card of Trust", "PAN", true));
docs.add(doc("KYC of Trustees / Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("SOCIETY")) {
docs.add(doc("Registration Certificate", "REG_CERT", true));
docs.add(doc("PAN Card of Society", "PAN", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("GOVERNMENT")) {
docs.add(doc("Government Order / Notification", "GOVT_ORDER", true));
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else if (type.contains("MSME")) {
docs.add(doc("Udyam Registration Certificate", "UDYAM", true));
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("GST Registration Certificate", "GST_CERT", false));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", true));
} else {
// Default non-individual
docs.add(doc("PAN Card", "PAN", true));
docs.add(doc("Registration Certificate", "REG_CERT", false));
docs.add(doc("KYC of Authorized Signatories", "SIGNATORY_KYC", false));
}
return docs;
}
private Map<String, Object> doc(final String name, final String code, final boolean mandatory) {
final Map<String, Object> d = new LinkedHashMap<>();
d.put("name", name);
d.put("code", code);
d.put("mandatory", mandatory);
return d;
}
// ─────────────────────────────────────────────────────────────────────────
// GET KYC for client – GET /v1/clients/{clientId}/kyc
// ─────────────────────────────────────────────────────────────────────────
@@ -173,11 +295,11 @@ public class ClientKycApiResource {
public CommandProcessingResult updateClientKyc(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@PathParam("kycId") @Parameter(description = "KYC record identifier") final Long kycId,
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.updateClientKyc(clientId, kycId)
.withJson(toApiJsonSerializer.serialize(clientKycRequest))
.withJson(apiRequestBodyAsJson)
.build();
return commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -224,11 +346,11 @@ public class ClientKycApiResource {
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@PathParam("kycId") @Parameter(description = "KYC record identifier") final Long kycId,
@QueryParam("command") @Parameter(
description = "Command: aadhaar-generate-otp | verify-pan | verify-aadhaar | verify-gstin | verify | reject")
description = "Command: aadhaar-generate-otp | verify-pan | verify-aadhaar | verify-gstin | verify-cin | verify-din | verify | reject")
final String command,
@Parameter(hidden = true) final ClientKycRequest clientKycRequest) {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final String json = toApiJsonSerializer.serialize(clientKycRequest);
final String json = apiRequestBodyAsJson;
LOG.info("KYC command received: clientId={}, kycId={}, command={}, requestBody={}", clientId, kycId, command, json);
@@ -245,6 +367,12 @@ public class ClientKycApiResource {
} else if ("verify-gstin".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder()
.verifyGstinClientKyc(clientId, kycId).withJson(json).build();
} else if ("verify-cin".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder()
.verifyCinClientKyc(clientId, kycId).withJson(json).build();
} else if ("verify-din".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder()
.verifyDinClientKyc(clientId, kycId).withJson(json).build();
} else if ("verify".equalsIgnoreCase(command)) {
commandRequest = new CommandWrapperBuilder().verifyClientKyc(clientId, kycId).withJson(json).build();
} else if ("reject".equalsIgnoreCase(command)) {
@@ -252,7 +380,7 @@ public class ClientKycApiResource {
} else {
throw new org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException(
"command", command,
"aadhaar-generate-otp", "verify-pan", "verify-aadhaar", "verify-gstin", "verify", "reject");
"aadhaar-generate-otp", "verify-pan", "verify-aadhaar", "verify-gstin", "verify-cin", "verify-din", "verify", "reject");
}
try {
@@ -0,0 +1,171 @@
/**
* 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.client.api;
import com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.data.ClientNomineeData;
import org.apache.fineract.portfolio.client.service.ClientNomineeReadPlatformService;
import org.apache.fineract.portfolio.client.service.ClientNomineeWritePlatformService;
import org.springframework.stereotype.Component;
@Path("/v1/clients/{clientId}/nominees")
@Component
@Tag(name = "Client Nominees", description = "Manage nominees for a client's accounts. "
+ "Nominees are linked to specific accounts (SB/CB/FD/RD/LA), not to clients directly. "
+ "Nominee feature is only available if the client has at least one account. "
+ "Total percentage share per account cannot exceed 100%.")
@RequiredArgsConstructor
public class ClientNomineeApiResource {
private static final String RESOURCE_NAME = "CLIENTNOMINEE";
private final PlatformSecurityContext context;
private final ClientNomineeReadPlatformService readService;
private final ClientNomineeWritePlatformService writeService;
private final DefaultToApiJsonSerializer<ClientNomineeData> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<List> listSerializer;
private final DefaultToApiJsonSerializer<CommandProcessingResult> resultSerializer;
private final FromJsonHelper fromJsonHelper;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve nominees for a client",
description = "Use accountNo + accountType query params to filter by a specific account. "
+ "Without these params, returns all nominees for the client.")
public String retrieveAll(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@QueryParam("accountNo") @Parameter(description = "Account number to filter by") final String accountNo,
@QueryParam("accountType") @Parameter(description = "Account type: SB, CB, FD, RD, LA") final String accountType) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final List<ClientNomineeData> nominees;
if (accountNo != null && accountType != null) {
nominees = readService.retrieveNomineesByAccount(clientId, accountNo, accountType);
} else {
nominees = readService.retrieveAllNominees(clientId);
}
return listSerializer.serialize(nominees);
}
@GET
@Path("{nomineeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a specific nominee")
public String retrieveOne(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@PathParam("nomineeId") @Parameter(description = "Nominee ID") final Long nomineeId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final ClientNomineeData data = readService.retrieveNominee(clientId, nomineeId);
return toApiJsonSerializer.serialize(data);
}
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve nominee template with dropdown options",
description = "Also returns hasAnyAccount flag — nominee feature is only available if the client has accounts.")
public String retrieveTemplate(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final ClientNomineeData template = readService.retrieveTemplate(clientId);
template.setHasAnyAccount(readService.clientHasAnyAccount(clientId));
return toApiJsonSerializer.serialize(template);
}
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Add a nominee to a client's account",
description = "accountNo and accountType are mandatory. Nominee must be an existing client. "
+ "Percentage share per account cannot exceed 100%.")
public String addNominee(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
// Gate: client must have at least one account
if (!readService.clientHasAnyAccount(clientId)) {
throw new PlatformDataIntegrityException("error.msg.nominee.no.accounts",
"Cannot add nominees. Client " + clientId + " has no accounts (savings, current, FD, RD, or loan).",
"clientId");
}
final JsonElement parsedCommand = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonCommand command = new JsonCommand(null, apiRequestBodyAsJson, parsedCommand,
this.fromJsonHelper, "CLIENTNOMINEE", clientId, null, null, clientId,
null, null, null, null, null, null, null, null, null);
final CommandProcessingResult result = writeService.addNominee(clientId, command);
return resultSerializer.serialize(result);
}
@PUT
@Path("{nomineeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a nominee")
public String updateNominee(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@PathParam("nomineeId") @Parameter(description = "Nominee ID") final Long nomineeId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasUpdatePermission(RESOURCE_NAME);
final JsonElement parsedCommand = this.fromJsonHelper.parse(apiRequestBodyAsJson);
final JsonCommand command = new JsonCommand(null, apiRequestBodyAsJson, parsedCommand,
this.fromJsonHelper, "CLIENTNOMINEE", clientId, null, null, clientId,
null, null, null, null, null, null, null, null, null);
final CommandProcessingResult result = writeService.updateNominee(clientId, nomineeId, command);
return resultSerializer.serialize(result);
}
@DELETE
@Path("{nomineeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a nominee")
public String deleteNominee(
@PathParam("clientId") @Parameter(description = "Client ID") final Long clientId,
@PathParam("nomineeId") @Parameter(description = "Nominee ID") final Long nomineeId) {
this.context.authenticatedUser().validateHasDeletePermission(RESOURCE_NAME);
final CommandProcessingResult result = writeService.deleteNominee(clientId, nomineeId);
return resultSerializer.serialize(result);
}
}
@@ -44,7 +44,9 @@ import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.data.AuditData;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.AuditReadPlatformService;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
import org.apache.fineract.infrastructure.bulkimport.service.BulkImportWorkbookPopulatorService;
@@ -56,6 +58,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamException;
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.utils.SQLBuilder;
import org.apache.fineract.infrastructure.core.service.CommandParameterUtil;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.Page;
@@ -74,6 +77,8 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Path("/v1/clients")
@@ -83,6 +88,8 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class ClientsApiResource {
private static final Logger LOG = LoggerFactory.getLogger(ClientsApiResource.class);
private final PlatformSecurityContext context;
private final ClientReadPlatformService clientReadPlatformService;
private final ClientTemplateReadPlatformService clientTemplateReadPlatformService;
@@ -96,6 +103,7 @@ public class ClientsApiResource {
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final GuarantorReadPlatformService guarantorReadPlatformService;
private final SqlValidator sqlValidator;
private final AuditReadPlatformService auditReadPlatformService;
@GET
@Path("template")
@@ -180,6 +188,8 @@ public class ClientsApiResource {
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ClientsApiResourceSwagger.PostClientsResponse.class)))
public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
LOG.info("Client CREATE request body: {}", apiRequestBodyAsJson);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createClient() //
.withJson(apiRequestBodyAsJson) //
@@ -588,4 +598,25 @@ public class ClientsApiResource {
return toApiJsonSerializer.serialize(transferDate);
}
@GET
@Path("{clientId}/audit")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve Client Activity Logs", description = "Returns audit trail for a specific client including all actions performed on the client and related entities.\n"
+ "\n" + "Example Requests:\n" + "\n" + "clients/1/audit\n" + "clients/1/audit?includeJson=true")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveClientAuditEntries(@PathParam("clientId") @Parameter(description = "clientId") final Long clientId,
@Context final UriInfo uriInfo,
@QueryParam("includeJson") @Parameter(description = "includeJson") final Boolean includeJson) {
context.authenticatedUser().validateHasReadPermission(ClientApiConstants.CLIENT_RESOURCE_NAME);
final SQLBuilder extraCriteria = new SQLBuilder();
extraCriteria.addCriteria("aud.client_id = ", clientId);
final boolean includeJsonFlag = Boolean.TRUE.equals(includeJson);
final List<AuditData> auditEntries = auditReadPlatformService.retrieveAuditEntries(extraCriteria, includeJsonFlag);
return toApiJsonSerializer.serialize(auditEntries);
}
}
@@ -77,28 +77,39 @@ public class ClientKycCommand {
v.reset().parameter(ClientKycApiConstants.kycTypeParam).value(kycType).notNull().inMinMaxRange(1, 2);
v.reset().parameter(ClientKycApiConstants.riskCategoryParam).value(riskCategory).notNull().inMinMaxRange(1, 3);
// PAN is mandatory as per RBI KYC guidelines
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber).notBlank()
.notExceedingLengthOf(10)
.matchesRegularExpression(ClientKycApiConstants.PAN_REGEX,
"PAN must be in format: 5 uppercase letters + 4 digits + 1 uppercase letter (e.g. ABCDE1234F)");
// PAN – optional; validated only if provided
if (panNumber != null && !panNumber.isBlank()) {
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
.notExceedingLengthOf(10)
.matchesRegularExpression(ClientKycApiConstants.PAN_REGEX,
"PAN must be in format: 5 uppercase letters + 4 digits + 1 uppercase letter (e.g. ABCDE1234F)");
}
// Aadhaar – mandatory for individual
if (kycType != null && kycType == 1) {
v.reset().parameter(ClientKycApiConstants.aadhaarNumberMaskedParam).value(aadhaarNumberMasked).notBlank()
// Aadhaar – optional; validated only if provided (mandatory for individual at verification time)
if (aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank()) {
v.reset().parameter(ClientKycApiConstants.aadhaarNumberMaskedParam).value(aadhaarNumberMasked)
.notExceedingLengthOf(12)
.matchesRegularExpression(ClientKycApiConstants.AADHAAR_MASKED_REGEX,
"Aadhaar masked number must be 12 characters (first 8 masked, last 4 digits visible)");
}
// GSTIN – mandatory for non-individual
if (kycType != null && kycType == 2) {
v.reset().parameter(ClientKycApiConstants.gstinNumberParam).value(gstinNumber).notBlank()
// GSTIN – optional; validated only if provided (mandatory for non-individual at verification time)
if (gstinNumber != null && !gstinNumber.isBlank()) {
v.reset().parameter(ClientKycApiConstants.gstinNumberParam).value(gstinNumber)
.notExceedingLengthOf(15)
.matchesRegularExpression(ClientKycApiConstants.GSTIN_REGEX,
"GSTIN must be a valid 15-character GST Identification Number");
}
// At least one ID document must be provided
final boolean hasPan = panNumber != null && !panNumber.isBlank();
final boolean hasAadhaar = aadhaarNumberMasked != null && !aadhaarNumberMasked.isBlank();
final boolean hasGstin = gstinNumber != null && !gstinNumber.isBlank();
if (!hasPan && !hasAadhaar && !hasGstin) {
v.reset().parameter(ClientKycApiConstants.panNumberParam).value(panNumber)
.notBlank();
}
throwExceptionIfValidationWarningsExist(errors);
}
@@ -184,6 +195,16 @@ public class ClientKycCommand {
// GSTIN is already stored on the KYC entity; nothing to validate in the command body.
}
/** verify-cin command: CIN is read from the existing KYC record – no required body fields. */
public void validateForVerifyCin() {
// CIN is already stored on the KYC entity; nothing to validate in the command body.
}
/** verify-din command: DIN is read from the existing KYC record – no required body fields. */
public void validateForVerifyDin() {
// DIN is already stored on the KYC entity; nothing to validate in the command body.
}
public void validateForReject() {
final List<ApiParameterError> errors = new ArrayList<>();
final DataValidatorBuilder v = new DataValidatorBuilder(errors).resource(ClientKycApiConstants.RESOURCE_NAME);
@@ -40,6 +40,7 @@ public class ClientAddressRequest implements Serializable {
private String addressLine2;
private String addressLine3;
private String townVillage;
private String street;
private String countyDistrict;
private Long stateProvinceId;
private BigDecimal latitude;
@@ -27,28 +27,40 @@ public class ClientApiCollectionConstants extends ClientApiConstants {
protected static final Set<String> CLIENT_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(familyMembers, address,
localeParamName, dateFormatParamName, groupIdParamName, accountNoParamName, cifNumberParamName, externalIdParamName, mobileNoParamName,
emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName,
alternateMobileNoParamName, emailAddressParamName, firstnameParamName, middlenameParamName, lastnameParamName, fullnameParamName, officeIdParamName,
activeParamName, activationDateParamName, staffIdParamName, submittedOnDateParamName, savingsProductIdParamName,
dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName,
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
nameVerifiedParamName, screeningLogIdParamName,
clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, datatables, isStaffParamName, kycParamName));
protected static final Set<String> CLIENT_NON_PERSON_CREATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(familyMembers, address, localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName,
incorpValidityTillParamName, constitutionIdParamName, mainBusinessLineIdParamName, datatables));
incorpValidityTillParamName, constitutionIdParamName, mainBusinessLineIdParamName,
authorizedSignatoryParamName, additionalInfoParamName, dateOfRegistrationParamName, datatables));
protected static final Set<String> CLIENT_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName,
dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, emailAddressParamName, firstnameParamName,
dateFormatParamName, accountNoParamName, externalIdParamName, mobileNoParamName, alternateMobileNoParamName, emailAddressParamName, firstnameParamName,
middlenameParamName,
lastnameParamName, fullnameParamName, activeParamName, activationDateParamName, staffIdParamName, savingsProductIdParamName,
dateOfBirthParamName, genderIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName,
dateOfBirthParamName, genderIdParamName, salutationIdParamName, clientTypeIdParamName, clientClassificationIdParamName,
occupationCategoryIdParamName, occupationIdParamName, maritalStatusIdParamName, monthlyIncomeParamName, annualTurnoverParamName,
ageYearsParamName, ageMonthsParamName, ageDaysParamName,
nationalityIdParamName, isMinorParamName, guardianClientIdParamName, guardianRelationParamName,
dipVerifiedParamName, dipLookupLogIdParamName, dipTimestampParamName,
alternateDipVerifiedParamName, alternateDipLookupLogIdParamName, alternateDipTimestampParamName,
nameVerifiedParamName, screeningLogIdParamName,
submittedOnDateParamName, clientNonPersonDetailsParamName, displaynameParamName, legalFormIdParamName, isStaffParamName));
protected static final Set<String> CLIENT_NON_PERSON_UPDATE_REQUEST_DATA_PARAMETERS = new HashSet<>(
Arrays.asList(localeParamName, dateFormatParamName, incorpNumberParamName, remarksParamName, incorpValidityTillParamName,
constitutionIdParamName, mainBusinessLineIdParamName));
constitutionIdParamName, mainBusinessLineIdParamName, authorizedSignatoryParamName, additionalInfoParamName,
dateOfRegistrationParamName));
/**
* These parameters will match the class level parameters of {@link ClientData}. Where possible, we try to get
@@ -166,6 +166,12 @@ public final class ClientDataValidator {
.notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateMobileNoParamName, element)) {
final String alternateMobileNo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.alternateMobileNoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.alternateMobileNoParamName).value(alternateMobileNo).ignoreIfNull()
.notExceedingLengthOf(50);
}
final Boolean active = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.activeParamName, element);
if (active != null) {
if (active.booleanValue()) {
@@ -225,6 +231,26 @@ public final class ClientDataValidator {
baseDataValidator.reset().parameter("isStaff").value(isStaffFlag).notNull();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.nationalityIdParamName, element)) {
final String nationality = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.nationalityIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.nationalityIdParamName).value(nationality).notBlank();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.isMinorParamName, element)) {
final Boolean isMinor = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.isMinorParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.isMinorParamName).value(isMinor).notNull();
if (Boolean.TRUE.equals(isMinor)) {
final Long guardianClientId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.guardianClientIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianClientIdParamName).value(guardianClientId).notNull()
.longGreaterThanZero();
final String guardianRelation = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.guardianRelationParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianRelationParamName).value(guardianRelation).notBlank();
}
}
if (this.configurationReadPlatformService.retrieveGlobalConfiguration(GlobalConfigurationConstants.ENABLE_ADDRESS).isEnabled()) {
final JsonArray address = this.fromApiJsonHelper.extractJsonArrayNamed(ClientApiConstants.address, element);
baseDataValidator.reset().parameter(ClientApiConstants.address).value(address).ignoreIfNull().jsonArrayNotEmpty();
@@ -276,6 +302,25 @@ public final class ClientDataValidator {
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.authorizedSignatoryParamName, element)) {
final String authorizedSignatory = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.authorizedSignatoryParamName).value(authorizedSignatory).ignoreIfNull()
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.additionalInfoParamName, element)) {
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.additionalInfoParamName).value(additionalInfo).ignoreIfNull()
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dateOfRegistrationParamName, element)) {
final LocalDate dateOfRegistration = this.fromApiJsonHelper
.extractLocalDateNamed(ClientApiConstants.dateOfRegistrationParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.dateOfRegistrationParamName).value(dateOfRegistration).ignoreIfNull();
}
return dataValidationErrors;
}
@@ -443,6 +488,13 @@ public final class ClientDataValidator {
baseDataValidator.reset().parameter(ClientApiConstants.mobileNoParamName).value(mobileNo).notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateMobileNoParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String alternateMobileNo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.alternateMobileNoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.alternateMobileNoParamName).value(alternateMobileNo).ignoreIfNull()
.notExceedingLengthOf(50);
}
final Boolean active = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.activeParamName, element);
if (active != null) {
atLeastOneParameterPassedForUpdate = true;
@@ -513,6 +565,53 @@ public final class ClientDataValidator {
baseDataValidator.reset().parameter("isStaff").value(isStaffFlag).notNull();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.nationalityIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String nationality = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.nationalityIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.nationalityIdParamName).value(nationality).notBlank();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.isMinorParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final Boolean isMinor = this.fromApiJsonHelper.extractBooleanNamed(ClientApiConstants.isMinorParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.isMinorParamName).value(isMinor).notNull();
if (Boolean.TRUE.equals(isMinor)) {
final Long guardianClientId = this.fromApiJsonHelper.extractLongNamed(ClientApiConstants.guardianClientIdParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianClientIdParamName).value(guardianClientId).notNull()
.longGreaterThanZero();
final String guardianRelation = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.guardianRelationParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.guardianRelationParamName).value(guardianRelation).notBlank();
}
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dipVerifiedParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dipLookupLogIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dipTimestampParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateDipVerifiedParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateDipLookupLogIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.alternateDipTimestampParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.nameVerifiedParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.screeningLogIdParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
}
Map<String, Object> parameterUpdateStatusDetails = getParameterUpdateStatusAndDataValidationErrorsForUpdateOnClientNonPerson(
element.getAsJsonObject().get(ClientApiConstants.clientNonPersonDetailsParamName));
boolean atLeastOneParameterPassedForClientNonPersonUpdate = (boolean) parameterUpdateStatusDetails.get("parameterUpdateStatus");
@@ -573,6 +672,28 @@ public final class ClientDataValidator {
.integerGreaterThanZero();
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.authorizedSignatoryParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String authorizedSignatory = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.authorizedSignatoryParamName).value(authorizedSignatory)
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.additionalInfoParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.additionalInfoParamName).value(additionalInfo)
.notExceedingLengthOf(500);
}
if (this.fromApiJsonHelper.parameterExists(ClientApiConstants.dateOfRegistrationParamName, element)) {
atLeastOneParameterPassedForUpdate = true;
final LocalDate dateOfRegistration = this.fromApiJsonHelper
.extractLocalDateNamed(ClientApiConstants.dateOfRegistrationParamName, element);
baseDataValidator.reset().parameter(ClientApiConstants.dateOfRegistrationParamName).value(dateOfRegistration);
}
Map<String, Object> parameterUpdateStatusDetails = new HashMap<>();
parameterUpdateStatusDetails.put("parameterUpdateStatus", atLeastOneParameterPassedForUpdate);
parameterUpdateStatusDetails.put("dataValidationErrors", dataValidationErrors);
@@ -0,0 +1,43 @@
/**
* 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.client.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public final class ClientForm60Data {
private final Long id;
private final Long clientId;
private final String clientDisplayName;
private final String fullName;
private final LocalDate dateOfBirth;
private final String fatherName;
private final String address;
private final String transactionType;
private final BigDecimal transactionAmount;
private final BigDecimal estimatedTotalIncome;
private final String reasonForNoPan;
private final Boolean declaration;
private final LocalDate submittedOn;
}
@@ -48,14 +48,50 @@ public final class ClientKycApiConstants {
/** One-time password entered by the client – required for the verify-otp step. */
public static final String aadhaarOtpParam = "aadhaarOtp";
// ── Extended KYC fields ──────────────────────────────────────────────────
public static final String panAadhaarSeedingStatusParam = "panAadhaarSeedingStatus";
public static final String enterprisePanParam = "enterprisePan";
public static final String cinNumberParam = "cinNumber";
public static final String dinNumberParam = "dinNumber";
public static final String verifyCinParam = "verifyCin";
public static final String verifyDinParam = "verifyDin";
public static final String incorporationDateParam = "incorporationDate";
public static final String kycDocumentNameParam = "kycDocumentName";
public static final String kycDocumentNumberParam = "kycDocumentNumber";
public static final String kycDocIssueDateParam = "kycDocIssueDate";
public static final String kycDocExpiryDateParam = "kycDocExpiryDate";
public static final String amlCheckStatusParam = "amlCheckStatus";
public static final String bannedEntityMatchParam = "bannedEntityMatch";
public static final String willfulDefaulterParam = "willfulDefaulter";
public static final String ecgcSalFlagParam = "ecgcSalFlag";
public static final String dormantFlagParam = "dormantFlag";
public static final String accountStatusParam = "accountStatus";
public static final String freezeReasonParam = "freezeReason";
public static final String freezeAuthorityParam = "freezeAuthority";
public static final String form60TypeParam = "form60Type";
public static final String form60DateParam = "form60Date";
public static final String form15ApplicableParam = "form15Applicable";
public static final String form15TypeParam = "form15Type";
public static final String form15FYParam = "form15FY";
// ── Supported parameters for initiate/update ──────────────────────────────
public static final Set<String> INITIATE_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
kycTypeParam, riskCategoryParam, panNumberParam, aadhaarNumberMaskedParam,
gstinNumberParam, remarksParam, "locale"));
gstinNumberParam, remarksParam, "locale", "dateFormat",
panAadhaarSeedingStatusParam, enterprisePanParam, cinNumberParam, dinNumberParam, incorporationDateParam,
kycDocumentNameParam, kycDocumentNumberParam, kycDocIssueDateParam, kycDocExpiryDateParam,
amlCheckStatusParam, bannedEntityMatchParam, willfulDefaulterParam, ecgcSalFlagParam,
dormantFlagParam, accountStatusParam, freezeReasonParam, freezeAuthorityParam,
form60TypeParam, form60DateParam, form15ApplicableParam, form15TypeParam, form15FYParam));
public static final Set<String> UPDATE_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
panNumberParam, aadhaarNumberMaskedParam, gstinNumberParam,
riskCategoryParam, remarksParam, "locale"));
riskCategoryParam, remarksParam, "locale", "dateFormat",
panAadhaarSeedingStatusParam, enterprisePanParam, cinNumberParam, dinNumberParam, incorporationDateParam,
kycDocumentNameParam, kycDocumentNumberParam, kycDocIssueDateParam, kycDocExpiryDateParam,
amlCheckStatusParam, bannedEntityMatchParam, willfulDefaulterParam, ecgcSalFlagParam,
dormantFlagParam, accountStatusParam, freezeReasonParam, freezeAuthorityParam,
form60TypeParam, form60DateParam, form15ApplicableParam, form15TypeParam, form15FYParam));
/** verify command: existing flags + Aadhaar OTP fields for the verify-otp step. */
public static final Set<String> VERIFY_REQUEST_PARAMS = new HashSet<>(Arrays.asList(
@@ -76,6 +112,12 @@ public final class ClientKycApiConstants {
/** verify-gstin command: GSTIN is already stored; no extra body params required. */
public static final Set<String> VERIFY_GSTIN_REQUEST_PARAMS = new HashSet<>();
/** verify-cin command: CIN is already stored; no extra body params required. */
public static final Set<String> VERIFY_CIN_REQUEST_PARAMS = new HashSet<>();
/** verify-din command: DIN is already stored; no extra body params required. */
public static final Set<String> VERIFY_DIN_REQUEST_PARAMS = new HashSet<>();
public static final Set<String> REJECT_REQUEST_PARAMS = new HashSet<>(Arrays.asList(remarksParam));
// ── PAN regex: 5 uppercase letters + 4 digits + 1 uppercase letter ────────
@@ -86,4 +128,10 @@ public final class ClientKycApiConstants {
// ── GSTIN: 2-digit state + 10-char PAN + 1 entity + Z + check digit ──────
public static final String GSTIN_REGEX = "^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$";
// ── CIN: 1 char status + 5 digit NIC + 2 char state + 4 digit year + 3 char ownership + 6 digit reg ──
public static final String CIN_REGEX = "^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$";
// ── DIN: 8-digit unique director identification number ──────────────────
public static final String DIN_REGEX = "^[0-9]{8}$";
}
@@ -46,6 +46,8 @@ public final class ClientKycData {
private final Boolean panVerified;
private final LocalDate panVerifiedOn;
private final String panFullName;
private final String panFirstName;
private final String panLastName;
private final String panAadhaarSeedingStatus;
private final String panCategory;
private final String panStatus;
@@ -60,6 +62,36 @@ public final class ClientKycData {
private final Boolean gstinVerified;
private final LocalDate gstinVerifiedOn;
// ── Enterprise / Non-Individual ──────────────────────────────────────────
private final String enterprisePan;
private final String cinNumber;
private final LocalDate incorporationDate;
// ── KYC Document ─────────────────────────────────────────────────────────
private final String kycDocumentName;
private final String kycDocumentNumber;
private final LocalDate kycDocIssueDate;
private final LocalDate kycDocExpiryDate;
// ── AML / Compliance ─────────────────────────────────────────────────────
private final String amlCheckStatus;
private final String bannedEntityMatch;
private final String willfulDefaulter;
private final String ecgcSalFlag;
private final String dormantFlag;
// ── Account status / Freeze ──────────────────────────────────────────────
private final String accountStatus;
private final String freezeReason;
private final String freezeAuthority;
// ── Form 60 / Form 15G/15H ───────────────────────────────────────────────
private final String form60Type;
private final LocalDate form60Date;
private final Boolean form15Applicable;
private final String form15Type;
private final String form15Fy;
// ── CKYCRR ────────────────────────────────────────────────────────────────
private final String ckycNumber;
@@ -48,6 +48,72 @@ public class ClientKycRequest {
@Schema(description = "15-character GSTIN – mandatory for NON_INDIVIDUAL clients", example = "27ABCDE1234F1Z5")
private String gstinNumber;
@Schema(description = "PAN-Aadhaar seeding/linking status", example = "LINKED")
private String panAadhaarSeedingStatus;
@Schema(description = "Enterprise PAN (for non-individual clients)", example = "ABCDE1234F")
private String enterprisePan;
@Schema(description = "Corporate Identification Number (21 characters)", example = "U12345MH2020PTC123456")
private String cinNumber;
@Schema(description = "Date of incorporation for non-individual clients", example = "01 April 2020")
private String incorporationDate;
@Schema(description = "Name of KYC document used for identification", example = "Aadhaar Card")
private String kycDocumentName;
@Schema(description = "KYC document number", example = "1234-5678-9012")
private String kycDocumentNumber;
@Schema(description = "KYC document issue date", example = "15 January 2020")
private String kycDocIssueDate;
@Schema(description = "KYC document expiry date (null if no expiry)", example = "null")
private String kycDocExpiryDate;
@Schema(description = "AML check status", example = "CLEAR")
private String amlCheckStatus;
@Schema(description = "Whether client matches banned entity list", example = "NO")
private String bannedEntityMatch;
@Schema(description = "Willful defaulter flag (RBI list)", example = "NO")
private String willfulDefaulter;
@Schema(description = "ECGC SAL (Specific Approval List) flag", example = "NO")
private String ecgcSalFlag;
@Schema(description = "Dormant account flag", example = "ACTIVE")
private String dormantFlag;
@Schema(description = "Account status", example = "ACTIVE")
private String accountStatus;
@Schema(description = "Reason for account freeze (null if not frozen)")
private String freezeReason;
@Schema(description = "Authority that ordered the freeze (null if not frozen)")
private String freezeAuthority;
@Schema(description = "Form 60 type (FORM_60 for clients without PAN)", example = "FORM_60")
private String form60Type;
@Schema(description = "Date of Form 60 submission")
private String form60Date;
@Schema(description = "Whether Form 15G/15H is applicable", example = "false")
private Boolean form15Applicable;
@Schema(description = "Form 15 type: 15G or 15H", example = "15G")
private String form15Type;
@Schema(description = "Financial year for Form 15G/15H")
private String form15FY;
@Schema(description = "Date format for date fields", example = "dd MMMM yyyy")
private String dateFormat;
@Schema(description = "Optional remarks / notes", example = "Documents submitted at branch")
private String remarks;
@@ -0,0 +1,74 @@
/**
* 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.client.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
@Getter
@Setter
@NoArgsConstructor
public class ClientNomineeData {
private Long id;
private Long clientId;
// Nominee — linked existing client
private Long nomineeClientId;
private String nomineeDisplayName;
private String nomineeCifNumber;
private String nomineeAccountNo;
private LocalDate nomineeDateOfBirth;
private String nomineeMobileNo;
// Relationship
private Long relationshipId;
private String relationshipName;
// Share & status
private BigDecimal percentageShare;
private Boolean isActive;
// Minor nominee guardian
private Boolean nomineeIsMinor;
private Long guardianClientId;
private String guardianDisplayName;
private String guardianCifNumber;
private Long guardianRelationshipId;
private String guardianRelationshipName;
private LocalDate effectiveDate;
private String remarks;
// Nominee identifier (auto-generated)
private String nomineeIdentifier;
private String accountNo;
private String accountType;
// Template options
private Collection<CodeValueData> relationshipOptions;
/** Set on template response — true if client has at least one account (SB/CB/FD/RD/LA) */
private Boolean hasAnyAccount;
}
@@ -86,6 +86,29 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
@Column(name = "remarks")
private String remarks;
// ── CBS extended fields ──────────────────────────────────────────────
@Column(name = "aadhaar_number", length = 12)
private String aadhaarNumber;
@Column(name = "pan_number", length = 10)
private String panNumber;
@Column(name = "nationality", length = 50)
private String nationality;
@Column(name = "is_alive")
private Boolean isAlive;
@ManyToOne
@JoinColumn(name = "linked_client_id")
private Client linkedClient;
@Column(name = "marriage_date")
private LocalDate marriageDate;
@Column(name = "marriage_authority", length = 50)
private String marriageAuthority;
private ClientFamilyMembers(final Client client, final String firstName, final String middleName, final String lastName,
final String qualification, final String mobileNumber, final Long age, final Boolean isDependent, final CodeValue relationship,
final CodeValue maritalStatus, final CodeValue gender, final LocalDate dateOfBirth, final CodeValue profession,
@@ -107,6 +130,7 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
this.nominationType = nominationType;
this.percentageShare = percentageShare;
this.remarks = remarks;
this.isAlive = true;
}
public ClientFamilyMembers() {
@@ -249,4 +273,24 @@ public class ClientFamilyMembers extends AbstractPersistableCustom<Long> {
this.remarks = remarks;
}
public String getAadhaarNumber() { return aadhaarNumber; }
public void setAadhaarNumber(String aadhaarNumber) { this.aadhaarNumber = aadhaarNumber; }
public String getPanNumber() { return panNumber; }
public void setPanNumber(String panNumber) { this.panNumber = panNumber; }
public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
public Boolean getIsAlive() { return isAlive; }
public void setIsAlive(Boolean isAlive) { this.isAlive = isAlive; }
public Client getLinkedClient() { return linkedClient; }
public void setLinkedClient(Client linkedClient) { this.linkedClient = linkedClient; }
public LocalDate getMarriageDate() { return marriageDate; }
public void setMarriageDate(LocalDate marriageDate) { this.marriageDate = marriageDate; }
public String getMarriageAuthority() { return marriageAuthority; }
public void setMarriageAuthority(String marriageAuthority) { this.marriageAuthority = marriageAuthority; }
}
@@ -64,9 +64,20 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
@Column(name = "remarks", length = 150, nullable = true)
private String remarks;
@Column(name = "authorized_signatory", length = 500, nullable = true)
private String authorizedSignatory;
@Column(name = "additional_info", length = 500, nullable = true)
private String additionalInfo;
@Column(name = "date_of_registration", nullable = true)
private LocalDate dateOfRegistration;
public static ClientNonPerson createNew(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine,
String incorpNumber, LocalDate incorpValidityTill, String remarks) {
return new ClientNonPerson(client, constitution, mainBusinessLine, incorpNumber, incorpValidityTill, remarks);
String incorpNumber, LocalDate incorpValidityTill, String remarks, String authorizedSignatory, String additionalInfo,
LocalDate dateOfRegistration) {
return new ClientNonPerson(client, constitution, mainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory,
additionalInfo, dateOfRegistration);
}
protected ClientNonPerson() {
@@ -74,7 +85,8 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
}
private ClientNonPerson(final Client client, final CodeValue constitution, final CodeValue mainBusinessLine, final String incorpNumber,
final LocalDate incorpValidityTill, final String remarks) {
final LocalDate incorpValidityTill, final String remarks, final String authorizedSignatory, final String additionalInfo,
final LocalDate dateOfRegistration) {
if (client != null) {
this.client = client;
}
@@ -97,6 +109,16 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
this.remarks = remarks.trim();
}
if (StringUtils.isNotBlank(authorizedSignatory)) {
this.authorizedSignatory = authorizedSignatory.trim();
}
if (StringUtils.isNotBlank(additionalInfo)) {
this.additionalInfo = additionalInfo.trim();
}
this.dateOfRegistration = dateOfRegistration;
validate(client);
}
@@ -192,6 +214,26 @@ public class ClientNonPerson extends AbstractPersistableCustom<Long> {
actualChanges.put(ClientApiConstants.mainBusinessLineIdParamName, newValue);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.authorizedSignatoryParamName, this.authorizedSignatory)) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.authorizedSignatoryParamName);
actualChanges.put(ClientApiConstants.authorizedSignatoryParamName, newValue);
this.authorizedSignatory = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.additionalInfoParamName, this.additionalInfo)) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.additionalInfoParamName);
actualChanges.put(ClientApiConstants.additionalInfoParamName, newValue);
this.additionalInfo = StringUtils.defaultIfEmpty(newValue, null);
}
if (command.isChangeInLocalDateParameterNamed(ClientApiConstants.dateOfRegistrationParamName, this.dateOfRegistration)) {
final String valueAsInput = command.stringValueOfParameterNamed(ClientApiConstants.dateOfRegistrationParamName);
actualChanges.put(ClientApiConstants.dateOfRegistrationParamName, valueAsInput);
actualChanges.put(ClientApiConstants.dateFormatParamName, dateFormatAsInput);
actualChanges.put(ClientApiConstants.localeParamName, localeAsInput);
this.dateOfRegistration = command.localDateValueOfParameterNamed(ClientApiConstants.dateOfRegistrationParamName);
}
// validate();
return actualChanges;
@@ -0,0 +1,191 @@
/**
* 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.client.entityclassification.api;
import com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.entityclassification.data.EntityClassificationData;
import org.apache.fineract.portfolio.client.entityclassification.service.EntityClassificationReadService;
import org.apache.fineract.portfolio.client.entityclassification.service.EntityClassificationWriteService;
import org.springframework.stereotype.Component;
@Path("/v1/clients/{clientId}/entityclassification")
@Component
@Tag(name = "Entity Classification", description = "Entity Classification for non-individual client onboarding. "
+ "Supports 6 regulatory categories: Startup India (DPIIT), Mudra Borrower (PMMY), "
+ "Export Oriented Unit (EOU), Import/Export (IEC/DGFT), Priority Sector Lending (PSL), "
+ "Non-Priority Sector (NPS), and MSME (Udyam Registration).")
@RequiredArgsConstructor
public class EntityClassificationApiResource {
private static final String RESOURCE_NAME = "ENTITYCLASSIFICATION";
private final PlatformSecurityContext context;
private final EntityClassificationReadService readService;
private final EntityClassificationWriteService writeService;
private final DefaultToApiJsonSerializer<EntityClassificationData> toApiJsonSerializer;
private final DefaultToApiJsonSerializer<CommandProcessingResult> resultSerializer;
private final FromJsonHelper fromJsonHelper;
@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve all entity classifications for a client",
description = "Returns all 6 entity classification categories for the given client.")
public String retrieveAll(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME);
final EntityClassificationData data = readService.retrieveAllClassifications(clientId);
return toApiJsonSerializer.serialize(data);
}
@POST
@Path("startup")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Startup India (DPIIT) classification",
description = "Creates or updates the Startup India entity classification for the given client.")
public String saveStartupIndia(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveStartupIndia(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("mudra")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Mudra Borrower (PMMY) classification",
description = "Creates or updates the Mudra Borrower entity classification for the given client.")
public String saveMudraBorrower(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveMudraBorrower(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("eou")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Export Oriented Unit (EOU) classification",
description = "Creates or updates the EOU entity classification for the given client.")
public String saveEou(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveEou(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("importexport")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Import/Export (IEC/DGFT) classification",
description = "Creates or updates the Import/Export entity classification for the given client.")
public String saveImportExport(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveImportExport(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("psl")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Priority Sector Lending (PSL) classification",
description = "Creates or updates the Priority Sector Lending entity classification for the given client.")
public String savePrioritySector(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.savePrioritySector(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("nps")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save Non-Priority Sector (NPS) classification",
description = "Creates or updates the Non-Priority Sector entity classification for the given client.")
public String saveNonPrioritySector(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveNonPrioritySector(clientId, command);
return resultSerializer.serialize(result);
}
@POST
@Path("msme")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Save MSME (Udyam Registration) classification",
description = "Creates or updates the MSME entity classification for the given client.")
public String saveMsme(
@PathParam("clientId") @Parameter(description = "Client identifier") final Long clientId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
this.context.authenticatedUser().validateHasCreatePermission(RESOURCE_NAME);
final JsonCommand command = toJsonCommand(clientId, apiRequestBodyAsJson);
final CommandProcessingResult result = writeService.saveMsme(clientId, command);
return resultSerializer.serialize(result);
}
private JsonCommand toJsonCommand(final Long clientId, final String json) {
final JsonElement parsedCommand = this.fromJsonHelper.parse(json);
return new JsonCommand(null, json, parsedCommand, this.fromJsonHelper,
"ENTITYCLASSIFICATION", clientId, null, null, clientId, null, null, null, null, null, null, null, null, null);
}
}
@@ -0,0 +1,41 @@
/**
* 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.client.entityclassification.data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Wrapper DTO containing all 7 entity classification categories for a client.
*/
@Getter
@Setter
@NoArgsConstructor
public class EntityClassificationData {
private Long clientId;
private StartupIndiaData startupIndia;
private MudraBorrowerData mudraBorrower;
private EouData eou;
private ImportExportData importExport;
private PrioritySectorData prioritySector;
private NonPrioritySectorData nonPrioritySector;
private MsmeData msme;
}
@@ -0,0 +1,47 @@
/**
* 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.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class EouData {
private Long id;
private Long clientId;
private Boolean enabled;
private String eouRegistrationNumber;
private String registeredDcZone;
private String bondLutNumber;
private LocalDate validityFrom;
private LocalDate validityTo;
private String gstinEou;
private BigDecimal annualExportObligation;
private BigDecimal nfeTarget;
private String softexFilingStatus;
private String brcSubmissionStatus;
private String adCategoryBankCode;
private LocalDate femaDeclarationDate;
}
@@ -0,0 +1,46 @@
/**
* 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.client.entityclassification.data;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class ImportExportData {
private Long id;
private Long clientId;
private Boolean enabled;
private String iecCode;
private LocalDate iecIssueDate;
private String iecStatus;
private String tradeType;
private String primaryHsCode;
private String keyTradingCountries;
private String adCategoryBankCode;
private String ecgcPolicyNumber;
private LocalDate ecgcPolicyExpiry;
private Boolean eefcAccountRequired;
private Boolean pcfcFacilityRequired;
private Boolean femaDeclarationObtained;
}
@@ -0,0 +1,49 @@
/**
* 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.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class MsmeData {
private Long id;
private Long clientId;
private Boolean enabled;
private String udyamRegistrationNumber;
private LocalDate registrationDate;
private String registrationStatus;
private String msmeClassification;
private String enterpriseType;
private String nicActivityCode;
private BigDecimal annualTurnover;
private BigDecimal investmentPlantMachinery;
private String financialYearAssessment;
private String socialCategoryOwner;
private String genderPrimaryOwner;
private Integer noOfEmployees;
private Boolean womenLedEnterprise;
private Boolean divyangPwdOwned;
}
@@ -0,0 +1,45 @@
/**
* 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.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class MudraBorrowerData {
private Long id;
private Long clientId;
private Boolean enabled;
private String mudraCategory;
private String mudraLoanAccountNo;
private String lendingInstitutionName;
private BigDecimal sanctionedAmount;
private LocalDate disbursementDate;
private String loanPurpose;
private String mudraCardNumber;
private BigDecimal currentOutstanding;
private String repaymentStatus;
private Boolean npaFlag;
}
@@ -0,0 +1,43 @@
/**
* 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.client.entityclassification.data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class NonPrioritySectorData {
private Long id;
private Long clientId;
private Boolean enabled;
private String industrySector;
private String nicCode;
private String industryDescription;
private String annualRevenueBand;
private String riskCategory;
private Integer customerRiskScore;
private String amlRiskClassification;
private Boolean pepFlag;
private Boolean amlWatchlistMatch;
private Boolean adverseMediaFlag;
}
@@ -0,0 +1,50 @@
/**
* 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.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class PrioritySectorData {
private Long id;
private Long clientId;
private Boolean enabled;
private String pslCategory;
private String pslSubCategory;
private String pslLoanPurposeCode;
private String pslEligibilityBasis;
private String fiddReportingCode;
private BigDecimal pslSanctionedLimit;
private String msmeClassification;
private String udyamRegistrationNumber;
private LocalDate udyamRegistrationDate;
private BigDecimal annualTurnover;
private BigDecimal investmentPlantMachinery;
private Boolean weakerSectionFlag;
private Boolean scStBorrowerFlag;
private Boolean womenEntrepreneurFlag;
private Boolean minorityCommunityFlag;
}
@@ -0,0 +1,49 @@
/**
* 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.client.entityclassification.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class StartupIndiaData {
private Long id;
private Long clientId;
private Boolean enabled;
private String dpiitRecognitionNumber;
private LocalDate recognitionDate;
private LocalDate certificateExpiryDate;
private String entityStage;
private String sectorAsDpiit;
private String nicIndustryCode;
private Boolean taxExemption80iac;
private Boolean sidbiFund;
private Boolean selfCertification;
private Boolean fastTrackPatent;
private Boolean govtTenderExemption;
private Boolean creditGuaranteeCgss;
private BigDecimal pslLoanLimit;
private String pslSubCategory;
}
@@ -0,0 +1,26 @@
/**
* 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.client.entityclassification.service;
import org.apache.fineract.portfolio.client.entityclassification.data.EntityClassificationData;
public interface EntityClassificationReadService {
EntityClassificationData retrieveAllClassifications(Long clientId);
}
@@ -0,0 +1,218 @@
/**
* 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.client.entityclassification.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.client.entityclassification.data.EntityClassificationData;
import org.apache.fineract.portfolio.client.entityclassification.data.EouData;
import org.apache.fineract.portfolio.client.entityclassification.data.ImportExportData;
import org.apache.fineract.portfolio.client.entityclassification.data.MudraBorrowerData;
import org.apache.fineract.portfolio.client.entityclassification.data.NonPrioritySectorData;
import org.apache.fineract.portfolio.client.entityclassification.data.PrioritySectorData;
import org.apache.fineract.portfolio.client.entityclassification.data.MsmeData;
import org.apache.fineract.portfolio.client.entityclassification.data.StartupIndiaData;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEou;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEouRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExport;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsme;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsmeRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExportRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrower;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrowerRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndia;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndiaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class EntityClassificationReadServiceImpl implements EntityClassificationReadService {
private final ClientStartupIndiaRepository startupIndiaRepository;
private final ClientMudraBorrowerRepository mudraBorrowerRepository;
private final ClientEouRepository eouRepository;
private final ClientImportExportRepository importExportRepository;
private final ClientPrioritySectorRepository prioritySectorRepository;
private final ClientNonPrioritySectorRepository nonPrioritySectorRepository;
private final ClientMsmeRepository msmeRepository;
@Override
public EntityClassificationData retrieveAllClassifications(final Long clientId) {
final EntityClassificationData data = new EntityClassificationData();
data.setClientId(clientId);
startupIndiaRepository.findByClientId(clientId).ifPresent(e -> data.setStartupIndia(toStartupIndiaData(e)));
mudraBorrowerRepository.findByClientId(clientId).ifPresent(e -> data.setMudraBorrower(toMudraBorrowerData(e)));
eouRepository.findByClientId(clientId).ifPresent(e -> data.setEou(toEouData(e)));
importExportRepository.findByClientId(clientId).ifPresent(e -> data.setImportExport(toImportExportData(e)));
prioritySectorRepository.findByClientId(clientId).ifPresent(e -> data.setPrioritySector(toPrioritySectorData(e)));
nonPrioritySectorRepository.findByClientId(clientId).ifPresent(e -> data.setNonPrioritySector(toNonPrioritySectorData(e)));
msmeRepository.findByClientId(clientId).ifPresent(e -> data.setMsme(toMsmeData(e)));
return data;
}
private StartupIndiaData toStartupIndiaData(final ClientStartupIndia e) {
final StartupIndiaData d = new StartupIndiaData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setDpiitRecognitionNumber(e.getDpiitRecognitionNumber());
d.setRecognitionDate(e.getRecognitionDate());
d.setCertificateExpiryDate(e.getCertificateExpiryDate());
d.setEntityStage(e.getEntityStage());
d.setSectorAsDpiit(e.getSectorAsDpiit());
d.setNicIndustryCode(e.getNicIndustryCode());
d.setTaxExemption80iac(e.getTaxExemption80iac());
d.setSidbiFund(e.getSidbiFund());
d.setSelfCertification(e.getSelfCertification());
d.setFastTrackPatent(e.getFastTrackPatent());
d.setGovtTenderExemption(e.getGovtTenderExemption());
d.setCreditGuaranteeCgss(e.getCreditGuaranteeCgss());
d.setPslLoanLimit(e.getPslLoanLimit());
d.setPslSubCategory(e.getPslSubCategory());
return d;
}
private MudraBorrowerData toMudraBorrowerData(final ClientMudraBorrower e) {
final MudraBorrowerData d = new MudraBorrowerData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setMudraCategory(e.getMudraCategory());
d.setMudraLoanAccountNo(e.getMudraLoanAccountNo());
d.setLendingInstitutionName(e.getLendingInstitutionName());
d.setSanctionedAmount(e.getSanctionedAmount());
d.setDisbursementDate(e.getDisbursementDate());
d.setLoanPurpose(e.getLoanPurpose());
d.setMudraCardNumber(e.getMudraCardNumber());
d.setCurrentOutstanding(e.getCurrentOutstanding());
d.setRepaymentStatus(e.getRepaymentStatus());
d.setNpaFlag(e.getNpaFlag());
return d;
}
private EouData toEouData(final ClientEou e) {
final EouData d = new EouData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setEouRegistrationNumber(e.getEouRegistrationNumber());
d.setRegisteredDcZone(e.getRegisteredDcZone());
d.setBondLutNumber(e.getBondLutNumber());
d.setValidityFrom(e.getValidityFrom());
d.setValidityTo(e.getValidityTo());
d.setGstinEou(e.getGstinEou());
d.setAnnualExportObligation(e.getAnnualExportObligation());
d.setNfeTarget(e.getNfeTarget());
d.setSoftexFilingStatus(e.getSoftexFilingStatus());
d.setBrcSubmissionStatus(e.getBrcSubmissionStatus());
d.setAdCategoryBankCode(e.getAdCategoryBankCode());
d.setFemaDeclarationDate(e.getFemaDeclarationDate());
return d;
}
private ImportExportData toImportExportData(final ClientImportExport e) {
final ImportExportData d = new ImportExportData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setIecCode(e.getIecCode());
d.setIecIssueDate(e.getIecIssueDate());
d.setIecStatus(e.getIecStatus());
d.setTradeType(e.getTradeType());
d.setPrimaryHsCode(e.getPrimaryHsCode());
d.setKeyTradingCountries(e.getKeyTradingCountries());
d.setAdCategoryBankCode(e.getAdCategoryBankCode());
d.setEcgcPolicyNumber(e.getEcgcPolicyNumber());
d.setEcgcPolicyExpiry(e.getEcgcPolicyExpiry());
d.setEefcAccountRequired(e.getEefcAccountRequired());
d.setPcfcFacilityRequired(e.getPcfcFacilityRequired());
d.setFemaDeclarationObtained(e.getFemaDeclarationObtained());
return d;
}
private PrioritySectorData toPrioritySectorData(final ClientPrioritySector e) {
final PrioritySectorData d = new PrioritySectorData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setPslCategory(e.getPslCategory());
d.setPslSubCategory(e.getPslSubCategory());
d.setPslLoanPurposeCode(e.getPslLoanPurposeCode());
d.setPslEligibilityBasis(e.getPslEligibilityBasis());
d.setFiddReportingCode(e.getFiddReportingCode());
d.setPslSanctionedLimit(e.getPslSanctionedLimit());
d.setMsmeClassification(e.getMsmeClassification());
d.setUdyamRegistrationNumber(e.getUdyamRegistrationNumber());
d.setUdyamRegistrationDate(e.getUdyamRegistrationDate());
d.setAnnualTurnover(e.getAnnualTurnover());
d.setInvestmentPlantMachinery(e.getInvestmentPlantMachinery());
d.setWeakerSectionFlag(e.getWeakerSectionFlag());
d.setScStBorrowerFlag(e.getScStBorrowerFlag());
d.setWomenEntrepreneurFlag(e.getWomenEntrepreneurFlag());
d.setMinorityCommunityFlag(e.getMinorityCommunityFlag());
return d;
}
private MsmeData toMsmeData(final ClientMsme e) {
final MsmeData d = new MsmeData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setUdyamRegistrationNumber(e.getUdyamRegistrationNumber());
d.setRegistrationDate(e.getRegistrationDate());
d.setRegistrationStatus(e.getRegistrationStatus());
d.setMsmeClassification(e.getMsmeClassification());
d.setEnterpriseType(e.getEnterpriseType());
d.setNicActivityCode(e.getNicActivityCode());
d.setAnnualTurnover(e.getAnnualTurnover());
d.setInvestmentPlantMachinery(e.getInvestmentPlantMachinery());
d.setFinancialYearAssessment(e.getFinancialYearAssessment());
d.setSocialCategoryOwner(e.getSocialCategoryOwner());
d.setGenderPrimaryOwner(e.getGenderPrimaryOwner());
d.setNoOfEmployees(e.getNoOfEmployees());
d.setWomenLedEnterprise(e.getWomenLedEnterprise());
d.setDivyangPwdOwned(e.getDivyangPwdOwned());
return d;
}
private NonPrioritySectorData toNonPrioritySectorData(final ClientNonPrioritySector e) {
final NonPrioritySectorData d = new NonPrioritySectorData();
d.setId(e.getId());
d.setClientId(e.getClient().getId());
d.setEnabled(e.getEnabled());
d.setIndustrySector(e.getIndustrySector());
d.setNicCode(e.getNicCode());
d.setIndustryDescription(e.getIndustryDescription());
d.setAnnualRevenueBand(e.getAnnualRevenueBand());
d.setRiskCategory(e.getRiskCategory());
d.setCustomerRiskScore(e.getCustomerRiskScore());
d.setAmlRiskClassification(e.getAmlRiskClassification());
d.setPepFlag(e.getPepFlag());
d.setAmlWatchlistMatch(e.getAmlWatchlistMatch());
d.setAdverseMediaFlag(e.getAdverseMediaFlag());
return d;
}
}
@@ -0,0 +1,39 @@
/**
* 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.client.entityclassification.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface EntityClassificationWriteService {
CommandProcessingResult saveStartupIndia(Long clientId, JsonCommand command);
CommandProcessingResult saveMudraBorrower(Long clientId, JsonCommand command);
CommandProcessingResult saveEou(Long clientId, JsonCommand command);
CommandProcessingResult saveImportExport(Long clientId, JsonCommand command);
CommandProcessingResult savePrioritySector(Long clientId, JsonCommand command);
CommandProcessingResult saveNonPrioritySector(Long clientId, JsonCommand command);
CommandProcessingResult saveMsme(Long clientId, JsonCommand command);
}
@@ -0,0 +1,338 @@
/**
* 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.client.entityclassification.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEou;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientEouRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExport;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsme;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMsmeRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientImportExportRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrower;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientMudraBorrowerRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientNonPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySector;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientPrioritySectorRepository;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndia;
import org.apache.fineract.portfolio.client.entityclassification.domain.ClientStartupIndiaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class EntityClassificationWriteServiceImpl implements EntityClassificationWriteService {
private final PlatformSecurityContext context;
private final ClientRepositoryWrapper clientRepository;
private final FromJsonHelper fromJsonHelper;
private final ClientStartupIndiaRepository startupIndiaRepository;
private final ClientMudraBorrowerRepository mudraBorrowerRepository;
private final ClientEouRepository eouRepository;
private final ClientImportExportRepository importExportRepository;
private final ClientPrioritySectorRepository prioritySectorRepository;
private final ClientNonPrioritySectorRepository nonPrioritySectorRepository;
private final ClientMsmeRepository msmeRepository;
@Transactional
@Override
public CommandProcessingResult saveStartupIndia(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientStartupIndia entity = startupIndiaRepository.findByClientId(clientId).orElseGet(() -> {
final ClientStartupIndia newEntity = new ClientStartupIndia();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setDpiitRecognitionNumber(strVal(command, "dpiitRecognitionNumber"));
entity.setRecognitionDate(dateVal(command, "recognitionDate"));
entity.setCertificateExpiryDate(dateVal(command, "certificateExpiryDate"));
entity.setEntityStage(strVal(command, "entityStage"));
entity.setSectorAsDpiit(strVal(command, "sectorAsDpiit"));
entity.setNicIndustryCode(strVal(command, "nicIndustryCode"));
entity.setTaxExemption80iac(boolVal(command, "taxExemption80iac"));
entity.setSidbiFund(boolVal(command, "sidbiFund"));
entity.setSelfCertification(boolVal(command, "selfCertification"));
entity.setFastTrackPatent(boolVal(command, "fastTrackPatent"));
entity.setGovtTenderExemption(boolVal(command, "govtTenderExemption"));
entity.setCreditGuaranteeCgss(boolVal(command, "creditGuaranteeCgss"));
entity.setPslLoanLimit(decVal(command, "pslLoanLimit"));
entity.setPslSubCategory(strVal(command, "pslSubCategory"));
startupIndiaRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveMudraBorrower(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientMudraBorrower entity = mudraBorrowerRepository.findByClientId(clientId).orElseGet(() -> {
final ClientMudraBorrower newEntity = new ClientMudraBorrower();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setMudraCategory(strVal(command, "mudraCategory"));
entity.setMudraLoanAccountNo(strVal(command, "mudraLoanAccountNo"));
entity.setLendingInstitutionName(strVal(command, "lendingInstitutionName"));
entity.setSanctionedAmount(decVal(command, "sanctionedAmount"));
entity.setDisbursementDate(dateVal(command, "disbursementDate"));
entity.setLoanPurpose(strVal(command, "loanPurpose"));
entity.setMudraCardNumber(strVal(command, "mudraCardNumber"));
entity.setCurrentOutstanding(decVal(command, "currentOutstanding"));
entity.setRepaymentStatus(strVal(command, "repaymentStatus"));
entity.setNpaFlag(boolVal(command, "npaFlag"));
mudraBorrowerRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveEou(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientEou entity = eouRepository.findByClientId(clientId).orElseGet(() -> {
final ClientEou newEntity = new ClientEou();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setEouRegistrationNumber(strVal(command, "eouRegistrationNumber"));
entity.setRegisteredDcZone(strVal(command, "registeredDcZone"));
entity.setBondLutNumber(strVal(command, "bondLutNumber"));
entity.setValidityFrom(dateVal(command, "validityFrom"));
entity.setValidityTo(dateVal(command, "validityTo"));
entity.setGstinEou(strVal(command, "gstinEou"));
entity.setAnnualExportObligation(decVal(command, "annualExportObligation"));
entity.setNfeTarget(decVal(command, "nfeTarget"));
entity.setSoftexFilingStatus(strVal(command, "softexFilingStatus"));
entity.setBrcSubmissionStatus(strVal(command, "brcSubmissionStatus"));
entity.setAdCategoryBankCode(strVal(command, "adCategoryBankCode"));
entity.setFemaDeclarationDate(dateVal(command, "femaDeclarationDate"));
eouRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveImportExport(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientImportExport entity = importExportRepository.findByClientId(clientId).orElseGet(() -> {
final ClientImportExport newEntity = new ClientImportExport();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setIecCode(strVal(command, "iecCode"));
entity.setIecIssueDate(dateVal(command, "iecIssueDate"));
entity.setIecStatus(strVal(command, "iecStatus"));
entity.setTradeType(strVal(command, "tradeType"));
entity.setPrimaryHsCode(strVal(command, "primaryHsCode"));
entity.setKeyTradingCountries(strVal(command, "keyTradingCountries"));
entity.setAdCategoryBankCode(strVal(command, "adCategoryBankCode"));
entity.setEcgcPolicyNumber(strVal(command, "ecgcPolicyNumber"));
entity.setEcgcPolicyExpiry(dateVal(command, "ecgcPolicyExpiry"));
entity.setEefcAccountRequired(boolVal(command, "eefcAccountRequired"));
entity.setPcfcFacilityRequired(boolVal(command, "pcfcFacilityRequired"));
entity.setFemaDeclarationObtained(boolVal(command, "femaDeclarationObtained"));
importExportRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult savePrioritySector(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientPrioritySector entity = prioritySectorRepository.findByClientId(clientId).orElseGet(() -> {
final ClientPrioritySector newEntity = new ClientPrioritySector();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setPslCategory(strVal(command, "pslCategory"));
entity.setPslSubCategory(strVal(command, "pslSubCategory"));
entity.setPslLoanPurposeCode(strVal(command, "pslLoanPurposeCode"));
entity.setPslEligibilityBasis(strVal(command, "pslEligibilityBasis"));
entity.setFiddReportingCode(strVal(command, "fiddReportingCode"));
entity.setPslSanctionedLimit(decVal(command, "pslSanctionedLimit"));
entity.setMsmeClassification(strVal(command, "msmeClassification"));
entity.setUdyamRegistrationNumber(strVal(command, "udyamRegistrationNumber"));
entity.setUdyamRegistrationDate(dateVal(command, "udyamRegistrationDate"));
entity.setAnnualTurnover(decVal(command, "annualTurnover"));
entity.setInvestmentPlantMachinery(decVal(command, "investmentPlantMachinery"));
entity.setWeakerSectionFlag(boolVal(command, "weakerSectionFlag"));
entity.setScStBorrowerFlag(boolVal(command, "scStBorrowerFlag"));
entity.setWomenEntrepreneurFlag(boolVal(command, "womenEntrepreneurFlag"));
entity.setMinorityCommunityFlag(boolVal(command, "minorityCommunityFlag"));
prioritySectorRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveNonPrioritySector(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientNonPrioritySector entity = nonPrioritySectorRepository.findByClientId(clientId).orElseGet(() -> {
final ClientNonPrioritySector newEntity = new ClientNonPrioritySector();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setIndustrySector(strVal(command, "industrySector"));
entity.setNicCode(strVal(command, "nicCode"));
entity.setIndustryDescription(strVal(command, "industryDescription"));
entity.setAnnualRevenueBand(strVal(command, "annualRevenueBand"));
entity.setRiskCategory(strVal(command, "riskCategory"));
entity.setCustomerRiskScore(intVal(command, "customerRiskScore"));
entity.setAmlRiskClassification(strVal(command, "amlRiskClassification"));
entity.setPepFlag(boolVal(command, "pepFlag"));
entity.setAmlWatchlistMatch(boolVal(command, "amlWatchlistMatch"));
entity.setAdverseMediaFlag(boolVal(command, "adverseMediaFlag"));
nonPrioritySectorRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
@Transactional
@Override
public CommandProcessingResult saveMsme(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
final ClientMsme entity = msmeRepository.findByClientId(clientId).orElseGet(() -> {
final ClientMsme newEntity = new ClientMsme();
newEntity.setClient(client);
return newEntity;
});
entity.setEnabled(boolVal(command, "enabled"));
entity.setUdyamRegistrationNumber(strVal(command, "udyamRegistrationNumber"));
entity.setRegistrationDate(dateVal(command, "registrationDate"));
entity.setRegistrationStatus(strVal(command, "registrationStatus"));
entity.setMsmeClassification(strVal(command, "msmeClassification"));
entity.setEnterpriseType(strVal(command, "enterpriseType"));
entity.setNicActivityCode(strVal(command, "nicActivityCode"));
entity.setAnnualTurnover(decVal(command, "annualTurnover"));
entity.setInvestmentPlantMachinery(decVal(command, "investmentPlantMachinery"));
entity.setFinancialYearAssessment(strVal(command, "financialYearAssessment"));
entity.setSocialCategoryOwner(strVal(command, "socialCategoryOwner"));
entity.setGenderPrimaryOwner(strVal(command, "genderPrimaryOwner"));
entity.setNoOfEmployees(intVal(command, "noOfEmployees"));
entity.setWomenLedEnterprise(boolVal(command, "womenLedEnterprise"));
entity.setDivyangPwdOwned(boolVal(command, "divyangPwdOwned"));
msmeRepository.saveAndFlush(entity);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(entity.getId())
.build();
}
// --- Helper methods for extracting values from JsonCommand ---
private String strVal(final JsonCommand command, final String paramName) {
return command.stringValueOfParameterNamed(paramName);
}
private Boolean boolVal(final JsonCommand command, final String paramName) {
if (command.parameterExists(paramName)) {
return command.booleanObjectValueOfParameterNamed(paramName);
}
return null;
}
private LocalDate dateVal(final JsonCommand command, final String paramName) {
return command.localDateValueOfParameterNamed(paramName);
}
private BigDecimal decVal(final JsonCommand command, final String paramName) {
return command.bigDecimalValueOfParameterNamed(paramName);
}
private Integer intVal(final JsonCommand command, final String paramName) {
if (command.parameterExists(paramName)) {
return command.integerValueOfParameterNamed(paramName);
}
return null;
}
}
@@ -0,0 +1,42 @@
/**
* 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.client.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.client.service.ClientForm60WritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "CLIENTFORM60", action = "CREATE")
@RequiredArgsConstructor
public class CreateClientForm60CommandHandler implements NewCommandSourceHandler {
private final ClientForm60WritePlatformService form60WritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return form60WritePlatformService.createForm60(command.getClientId(), command);
}
}
@@ -0,0 +1,49 @@
/**
* 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.client.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Handles the {@code verify-cin} KYC command.
*
* <p>Calls LincPay {@code /validate-company} with the CIN stored on the KYC record.
* On success, marks {@code cinVerified = true} on the KYC record.
*/
@Service
@CommandType(entity = "CLIENTKYC", action = "VERIFYCIN")
@RequiredArgsConstructor
public class VerifyCinClientKycCommandHandler implements NewCommandSourceHandler {
private final ClientKycWritePlatformService clientKycWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return clientKycWritePlatformService.verifyCin(
command.getClientId(), command.entityId(), command);
}
}
@@ -0,0 +1,49 @@
/**
* 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.client.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.client.service.ClientKycWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Handles the {@code verify-din} KYC command.
*
* <p>Calls LincPay {@code /validate-director} with the DIN stored on the KYC record.
* On success, marks {@code dinVerified = true} on the KYC record.
*/
@Service
@CommandType(entity = "CLIENTKYC", action = "VERIFYDIN")
@RequiredArgsConstructor
public class VerifyDinClientKycCommandHandler implements NewCommandSourceHandler {
private final ClientKycWritePlatformService clientKycWritePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return clientKycWritePlatformService.verifyDin(
command.getClientId(), command.entityId(), command);
}
}
@@ -74,4 +74,20 @@ public interface KycVerificationService {
* @return verification result
*/
KycVerificationResult verifyGstin(String gstinNumber);
/**
* Verify a CIN (Corporate Identification Number) via the MCA database through LincPay API.
*
* @param cinNumber 21-character CIN (e.g. {@code U12345MH2020PTC123456})
* @return verification result containing company master data
*/
KycVerificationResult verifyCin(String cinNumber);
/**
* Verify a DIN (Director Identification Number) via the MCA database through LincPay API.
*
* @param dinNumber 8-digit DIN
* @return verification result containing director and associated company data
*/
KycVerificationResult verifyDin(String dinNumber);
}
@@ -67,4 +67,18 @@ public class KycVerificationServiceImpl implements KycVerificationService {
return KycVerificationResult.success("STUB-GSTIN-REF-" + gstinNumber,
"{\"status\":\"SUCCESS\",\"message\":\"Stub: GSTIN verified\",\"gstin\":\"" + gstinNumber + "\"}");
}
@Override
public KycVerificationResult verifyCin(final String cinNumber) {
LOG.info("[KYC-STUB] CIN verification called for CIN: {}. Replace with LincPay.", cinNumber);
return KycVerificationResult.success("STUB-CIN-REF-" + cinNumber,
"{\"status\":\"SUCCESS\",\"message\":\"Stub: CIN verified\",\"cin\":\"" + cinNumber + "\"}");
}
@Override
public KycVerificationResult verifyDin(final String dinNumber) {
LOG.info("[KYC-STUB] DIN verification called for DIN: {}. Replace with LincPay.", dinNumber);
return KycVerificationResult.success("STUB-DIN-REF-" + dinNumber,
"{\"status\":\"SUCCESS\",\"message\":\"Stub: DIN verified\",\"din\":\"" + dinNumber + "\"}");
}
}
@@ -90,6 +90,12 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
@Value("${fineract.kyc.lincpay.path.gstin-validate}")
private String gstinValidatePath;
@Value("${fineract.kyc.lincpay.path.cin-validate}")
private String cinValidatePath;
@Value("${fineract.kyc.lincpay.path.din-validate}")
private String dinValidatePath;
private final RestTemplate restTemplate;
public LincPayKycVerificationServiceImpl() {
@@ -274,6 +280,88 @@ public class LincPayKycVerificationServiceImpl implements KycVerificationService
}
}
// ── CIN Validation (Company Identification Number) ─────────────────────
/**
* POST https://ekyc.lincpay.in/EKYC/ekyc/validate-company
* Body: { "apiKey": "...", "cin": "...", "secretKey": "..." }
*/
@Override
public KycVerificationResult verifyCin(final String cinNumber) {
final String url = baseUrl + cinValidatePath;
try {
final JsonObject requestBody = new JsonObject();
requestBody.addProperty("apiKey", apiKey);
requestBody.addProperty("cin", cinNumber);
requestBody.addProperty("secretKey", secret);
LOG.info("LincPay CIN request URL: {}", url);
final String rawResponse = post(url, requestBody.toString());
LOG.info("LincPay CIN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (isSuccess(response)) {
LOG.info("LincPay CIN verified successfully for CIN: {}", cinNumber);
return KycVerificationResult.success("CIN-" + cinNumber, rawResponse);
}
final String errorMsg = extractMessage(response);
LOG.warn("LincPay CIN validation failed for CIN {}: {}", cinNumber, errorMsg);
return KycVerificationResult.failure(errorMsg, rawResponse);
} catch (final HttpClientErrorException | HttpServerErrorException e) {
LOG.error("LincPay CIN validation HTTP error for CIN {}: {} {}", cinNumber,
e.getStatusCode(), e.getResponseBodyAsString());
return KycVerificationResult.failure(
"CIN verification failed (HTTP " + e.getStatusCode() + "): "
+ e.getResponseBodyAsString(), null);
} catch (final Exception e) {
LOG.error("LincPay CIN validation unexpected error for CIN: {}", cinNumber, e);
return KycVerificationResult.failure("CIN verification failed: " + e.getMessage(), null);
}
}
// ── DIN Validation (Director Identification Number) ──────────────────────
/**
* POST https://ekyc.lincpay.in/EKYC/ekyc/validate-director
* Body: { "apiKey": "...", "din": "...", "secretKey": "..." }
*/
@Override
public KycVerificationResult verifyDin(final String dinNumber) {
final String url = baseUrl + dinValidatePath;
try {
final JsonObject requestBody = new JsonObject();
requestBody.addProperty("apiKey", apiKey);
requestBody.addProperty("din", dinNumber);
requestBody.addProperty("secretKey", secret);
LOG.info("LincPay DIN request URL: {}", url);
final String rawResponse = post(url, requestBody.toString());
LOG.info("LincPay DIN raw response: {}", rawResponse);
final JsonObject response = parseJson(rawResponse);
if (isSuccess(response)) {
LOG.info("LincPay DIN verified successfully for DIN: {}", dinNumber);
return KycVerificationResult.success("DIN-" + dinNumber, rawResponse);
}
final String errorMsg = extractMessage(response);
LOG.warn("LincPay DIN validation failed for DIN {}: {}", dinNumber, errorMsg);
return KycVerificationResult.failure(errorMsg, rawResponse);
} catch (final HttpClientErrorException | HttpServerErrorException e) {
LOG.error("LincPay DIN validation HTTP error for DIN {}: {} {}", dinNumber,
e.getStatusCode(), e.getResponseBodyAsString());
return KycVerificationResult.failure(
"DIN verification failed (HTTP " + e.getStatusCode() + "): "
+ e.getResponseBodyAsString(), null);
} catch (final Exception e) {
LOG.error("LincPay DIN validation unexpected error for DIN: {}", dinNumber, e);
return KycVerificationResult.failure("DIN verification failed: " + e.getMessage(), null);
}
}
// ── Internal helpers ──────────────────────────────────────────────────────
private String post(final String url, final String requestBodyJson) {
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.client.mapper;
import java.time.LocalDate;
import java.time.Period;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
@@ -27,8 +29,10 @@ import org.apache.fineract.portfolio.client.data.ClientTimelineData;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
import org.apache.fineract.useradministration.domain.AppUser;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;
@Mapper(config = MapstructMapperConfig.class)
@@ -43,6 +47,8 @@ public interface ClientMapper {
@Mapping(target = "transferToOfficeName", source = "transferToOffice.name")
@Mapping(target = "externalId", source = "externalId")
@Mapping(target = "gender", source = "source", qualifiedByName = "clientGenderCode")
@Mapping(target = "salutation", source = "source", qualifiedByName = "salutationCode")
@Mapping(target = "salutationOptions", ignore = true)
@Mapping(target = "imageId", source = "imageId")
@Mapping(target = "staffId", source = "staff.id")
@Mapping(target = "staffName", source = "staff.displayName")
@@ -54,10 +60,16 @@ public interface ClientMapper {
@Mapping(target = "clientClassification", source = "source", qualifiedByName = "clientClassificationCode")
@Mapping(target = "occupationCategory", source = "source", qualifiedByName = "occupationCategoryCode")
@Mapping(target = "occupation", source = "source", qualifiedByName = "occupationCode")
@Mapping(target = "maritalStatus", source = "source", qualifiedByName = "maritalStatusCode")
@Mapping(target = "occupationCategoryOptions", ignore = true)
@Mapping(target = "occupationOptions", ignore = true)
@Mapping(target = "legalForm", source = "source", qualifiedByName = "clientLegalFormEnum")
@Mapping(target = "isStaff", source = "source", qualifiedByName = "clientIsStaff")
@Mapping(target = "ageYears", ignore = true)
@Mapping(target = "ageMonths", ignore = true)
@Mapping(target = "ageDays", ignore = true)
@Mapping(target = "guardianClientId", source = "source", qualifiedByName = "guardianClientIdValue")
@Mapping(target = "guardianClientDisplayName", source = "source", qualifiedByName = "guardianClientDisplayNameValue")
@Mapping(target = "imagePresent", ignore = true)
@Mapping(target = "officeOptions", ignore = true)
@Mapping(target = "staffOptions", ignore = true)
@@ -122,6 +134,24 @@ public interface ClientMapper {
return CodeValueData.instance(code.getId(), code.getLabel());
}
@Named("salutationCode")
default CodeValueData salutationCode(Client client) {
final CodeValue code = client.getSalutation();
if (code == null) {
return null;
}
return CodeValueData.instance(code.getId(), code.getLabel());
}
@Named("maritalStatusCode")
default CodeValueData maritalStatusCode(Client client) {
final CodeValue code = client.getMaritalStatus();
if (code == null) {
return null;
}
return CodeValueData.instance(code.getId(), code.getLabel());
}
@Named("clientSubStatusCode")
default CodeValueData clientSubStatusCode(Client client) {
final CodeValue code = client.getSubStatus();
@@ -178,4 +208,25 @@ public interface ClientMapper {
return Boolean.valueOf(client.isStaff());
}
@Named("guardianClientIdValue")
default Long guardianClientIdValue(Client client) {
return client.getGuardianClient() != null ? client.getGuardianClient().getId() : null;
}
@Named("guardianClientDisplayNameValue")
default String guardianClientDisplayNameValue(Client client) {
return client.getGuardianClient() != null ? client.getGuardianClient().getDisplayName() : null;
}
@AfterMapping
default void computeAge(Client source, @MappingTarget ClientData target) {
final LocalDate dob = source.getDateOfBirth();
if (dob != null) {
final Period age = Period.between(dob, LocalDate.now());
target.setAgeYears(age.getYears());
target.setAgeMonths(age.getMonths());
target.setAgeDays(age.getDays());
}
}
}
@@ -62,10 +62,25 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
public static final String NOMINATION_TYPE = "nominationType";
public static final String PERCENTAGE_SHARE = "percentageShare";
public static final String REMARKS = "remarks";
public static final String AADHAAR_NUMBER = "aadhaarNumber";
public static final String PAN_NUMBER = "panNumber";
public static final String NATIONALITY = "nationality";
public static final String IS_ALIVE = "isAlive";
public static final String LINKED_CLIENT_ID = "linkedClientId";
public static final String MARRIAGE_DATE = "marriageDate";
public static final String MARRIAGE_AUTHORITY = "marriageAuthority";
private static final java.util.regex.Pattern AADHAAR_PATTERN = java.util.regex.Pattern.compile("^\\d{12}$");
private static final java.util.regex.Pattern PAN_PATTERN = java.util.regex.Pattern.compile("^[A-Z]{5}[0-9]{4}[A-Z]$");
private static final Set<String> VALID_MARRIAGE_AUTHORITIES = Set.of(
"SUB_REGISTRAR_OFFICE", "MARRIAGE_REGISTRAR", "COURT_MARRIAGE_OFFICER",
"MUNICIPAL_AUTHORITY", "EMBASSY_CONSULATE", "DEFENCE_AUTHORITY");
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(
Arrays.asList(ID, CLIENT_ID, FIRST_NAME, MIDDLE_NAME, LAST_NAME, QUALIFICATION, MOBILE_NUMBER, AGE, IS_DEPENDENT,
RELATIONSHIP_ID, MARITAL_STATUS_ID, GENDER_ID, DATE_OF_BIRTH, PROFESSION_ID, LOCALE, DATE_FORMAT, FAMILY_MEMBERS,
NOMINATION_TYPE, PERCENTAGE_SHARE, REMARKS));
NOMINATION_TYPE, PERCENTAGE_SHARE, REMARKS,
AADHAAR_NUMBER, PAN_NUMBER, NATIONALITY, IS_ALIVE, LINKED_CLIENT_ID, MARRIAGE_DATE, MARRIAGE_AUTHORITY));
public static final String FAMILY_MEMBERS1 = "FamilyMembers";
public static final String RELATION_SHIP_ID = "relationShipId";
private final FromJsonHelper fromApiJsonHelper;
@@ -207,6 +222,44 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
baseDataValidator.reset().parameter(REMARKS).value(remarks).notExceedingLengthOf(500);
}
// CBS extended field validations
if (this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element) != null) {
final String aadhaar = this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element);
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar).notExceedingLengthOf(12);
if (aadhaar != null && !aadhaar.isEmpty() && !AADHAAR_PATTERN.matcher(aadhaar).matches()) {
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar)
.failWithCode("invalid.format", "Aadhaar number must be exactly 12 digits");
}
}
if (this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element) != null) {
final String pan = this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element);
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan).notExceedingLengthOf(10);
if (pan != null && !pan.isEmpty() && !PAN_PATTERN.matcher(pan).matches()) {
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan)
.failWithCode("invalid.format", "PAN must match format ABCDE1234F");
}
}
if (this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element) != null) {
final String nationality = this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element);
baseDataValidator.reset().parameter(NATIONALITY).value(nationality).notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element) != null) {
final String authority = this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element);
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority).notExceedingLengthOf(50);
if (authority != null && !authority.isEmpty() && !VALID_MARRIAGE_AUTHORITIES.contains(authority)) {
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority)
.failWithCode("invalid.value", "Must be one of: " + VALID_MARRIAGE_AUTHORITIES);
}
}
if (this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element) != null) {
final long linkedId = this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element);
baseDataValidator.reset().parameter(LINKED_CLIENT_ID).value(linkedId).longGreaterThanZero();
}
}
public void validateForUpdate(final long familyMemberId, String json) {
@@ -292,6 +345,44 @@ public final class ClientFamilyMemberCommandFromApiJsonDeserializer {
baseDataValidator.reset().parameter(REMARKS).value(remarks).notExceedingLengthOf(500);
}
// CBS extended field validations
if (this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element) != null) {
final String aadhaar = this.fromApiJsonHelper.extractStringNamed(AADHAAR_NUMBER, element);
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar).notExceedingLengthOf(12);
if (aadhaar != null && !aadhaar.isEmpty() && !AADHAAR_PATTERN.matcher(aadhaar).matches()) {
baseDataValidator.reset().parameter(AADHAAR_NUMBER).value(aadhaar)
.failWithCode("invalid.format", "Aadhaar number must be exactly 12 digits");
}
}
if (this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element) != null) {
final String pan = this.fromApiJsonHelper.extractStringNamed(PAN_NUMBER, element);
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan).notExceedingLengthOf(10);
if (pan != null && !pan.isEmpty() && !PAN_PATTERN.matcher(pan).matches()) {
baseDataValidator.reset().parameter(PAN_NUMBER).value(pan)
.failWithCode("invalid.format", "PAN must match format ABCDE1234F");
}
}
if (this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element) != null) {
final String nationality = this.fromApiJsonHelper.extractStringNamed(NATIONALITY, element);
baseDataValidator.reset().parameter(NATIONALITY).value(nationality).notExceedingLengthOf(50);
}
if (this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element) != null) {
final String authority = this.fromApiJsonHelper.extractStringNamed(MARRIAGE_AUTHORITY, element);
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority).notExceedingLengthOf(50);
if (authority != null && !authority.isEmpty() && !VALID_MARRIAGE_AUTHORITIES.contains(authority)) {
baseDataValidator.reset().parameter(MARRIAGE_AUTHORITY).value(authority)
.failWithCode("invalid.value", "Must be one of: " + VALID_MARRIAGE_AUTHORITIES);
}
}
if (this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element) != null) {
final long linkedId = this.fromApiJsonHelper.extractLongNamed(LINKED_CLIENT_ID, element);
baseDataValidator.reset().parameter(LINKED_CLIENT_ID).value(linkedId).longGreaterThanZero();
}
}
public void validateForDelete(final long familyMemberId) {
@@ -57,10 +57,15 @@ public class ClientFamilyMembersReadPlatformServiceImpl implements ClientFamilyM
+ "fmb.lastname AS lastName,fmb.qualification AS qualification,fmb.mobile_number as mobileNumber,fmb.age as age,fmb.is_dependent as isDependent,cv.code_value AS relationship,fmb.relationship_cv_id AS relationshipId,"
+ "c.code_value AS maritalStatus,fmb.marital_status_cv_id AS maritalStatusId,"
+ "c1.code_value AS gender, fmb.gender_cv_id AS genderId, fmb.date_of_birth AS dateOfBirth, c2.code_value AS profession, fmb.profession_cv_id AS professionId,"
+ "fmb.nomination_type AS nominationType, fmb.percentage_share AS percentageShare, fmb.remarks AS remarks"
+ "fmb.nomination_type AS nominationType, fmb.percentage_share AS percentageShare, fmb.remarks AS remarks,"
+ "fmb.aadhaar_number AS aadhaarNumber, fmb.pan_number AS panNumber, fmb.nationality AS nationality,"
+ "fmb.is_alive AS isAlive, fmb.linked_client_id AS linkedClientId, fmb.marriage_date AS marriageDate,"
+ "fmb.marriage_authority AS marriageAuthority,"
+ "lc.display_name AS linkedClientName, lc.cif_number AS linkedClientCif"
+ " FROM m_family_members fmb" + " LEFT JOIN m_code_value cv ON fmb.relationship_cv_id=cv.id"
+ " LEFT JOIN m_code_value c ON fmb.marital_status_cv_id=c.id" + " LEFT JOIN m_code_value c1 ON fmb.gender_cv_id=c1.id"
+ " LEFT JOIN m_code_value c2 ON fmb.profession_cv_id=c2.id";
+ " LEFT JOIN m_code_value c2 ON fmb.profession_cv_id=c2.id"
+ " LEFT JOIN m_client lc ON fmb.linked_client_id=lc.id";
}
@Override
@@ -86,12 +91,24 @@ public class ClientFamilyMembersReadPlatformServiceImpl implements ClientFamilyM
final String nominationType = rs.getString("nominationType");
final BigDecimal percentageShare = rs.getBigDecimal("percentageShare");
final String remarks = rs.getString("remarks");
final String aadhaarNumber = rs.getString("aadhaarNumber");
final String panNumber = rs.getString("panNumber");
final String nationality = rs.getString("nationality");
final Boolean isAlive = rs.getObject("isAlive") != null ? rs.getBoolean("isAlive") : null;
final Long linkedClientId = JdbcSupport.getLong(rs, "linkedClientId");
final String linkedClientName = rs.getString("linkedClientName");
final String linkedClientCif = rs.getString("linkedClientCif");
final LocalDate marriageDate = JdbcSupport.getLocalDate(rs, "marriageDate");
final String marriageAuthority = rs.getString("marriageAuthority");
return ClientFamilyMembersData.builder().id(id).clientId(clientId).firstName(firstName).middleName(middleName)
.lastName(lastName).qualification(qualification).mobileNumber(mobileNumber).age(age).isDependent(isDependent)
.relationship(relationship).relationshipId(relationshipId).maritalStatus(maritalStatus).maritalStatusId(maritalStatusId)
.gender(gender).genderId(genderId).dateOfBirth(dateOfBirth).profession(profession).professionId(professionId)
.nominationType(nominationType).percentageShare(percentageShare).remarks(remarks).build();
.nominationType(nominationType).percentageShare(percentageShare).remarks(remarks)
.aadhaarNumber(aadhaarNumber).panNumber(panNumber).nationality(nationality)
.isAlive(isAlive).linkedClientId(linkedClientId).linkedClientName(linkedClientName)
.linkedClientCif(linkedClientCif).marriageDate(marriageDate).marriageAuthority(marriageAuthority).build();
}
}
@@ -129,6 +129,19 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
mobileNumber, age, isDependent, relationship, maritalStatus, gender, dateOfBirth, profession,
nominationType, percentageShare, remarks);
// CBS extended fields
clientFamilyMembers.setAadhaarNumber(command.stringValueOfParameterNamed("aadhaarNumber"));
clientFamilyMembers.setPanNumber(command.stringValueOfParameterNamed("panNumber"));
clientFamilyMembers.setNationality(command.stringValueOfParameterNamed("nationality"));
final Boolean isAlive = command.booleanObjectValueOfParameterNamed("isAlive");
clientFamilyMembers.setIsAlive(isAlive != null ? isAlive : true);
clientFamilyMembers.setMarriageDate(command.localDateValueOfParameterNamed("marriageDate"));
clientFamilyMembers.setMarriageAuthority(command.stringValueOfParameterNamed("marriageAuthority"));
final Long linkedClientId = command.longValueOfParameterNamed("linkedClientId");
if (linkedClientId != null) {
clientFamilyMembers.setLinkedClient(clientRepositoryWrapper.findOneWithNotFoundDetection(linkedClientId));
}
this.clientFamilyRepository.saveAndFlush(clientFamilyMembers);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientFamilyMembers.getId()).build();
@@ -258,6 +271,34 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
isDependent, relationship, maritalStatus, gender, dateOfBirth, profession,
nominationType, percentageShare, remarks);
// CBS extended fields
if (member.get("aadhaarNumber") != null && !member.get("aadhaarNumber").isJsonNull()) {
familyMember.setAadhaarNumber(member.get("aadhaarNumber").getAsString());
}
if (member.get("panNumber") != null && !member.get("panNumber").isJsonNull()) {
familyMember.setPanNumber(member.get("panNumber").getAsString());
}
if (member.get("nationality") != null && !member.get("nationality").isJsonNull()) {
familyMember.setNationality(member.get("nationality").getAsString());
}
if (member.get("isAlive") != null && !member.get("isAlive").isJsonNull()) {
familyMember.setIsAlive(member.get("isAlive").getAsBoolean());
}
if (member.get("linkedClientId") != null && !member.get("linkedClientId").isJsonNull()) {
familyMember.setLinkedClient(this.clientRepositoryWrapper.findOneWithNotFoundDetection(member.get("linkedClientId").getAsLong()));
}
if (member.get("marriageDate") != null && !member.get("marriageDate").isJsonNull()) {
try {
DateTimeFormatter fmt = new DateTimeFormatterBuilder().appendPattern(member.get("dateFormat").getAsString()).toFormatter();
familyMember.setMarriageDate(LocalDate.parse(member.get("marriageDate").getAsString(), fmt));
} catch (DateTimeParseException e) {
LOG.warn("Could not parse marriageDate", e);
}
}
if (member.get("marriageAuthority") != null && !member.get("marriageAuthority").isJsonNull()) {
familyMember.setMarriageAuthority(member.get("marriageAuthority").getAsString());
}
this.clientFamilyRepository.saveAndFlush(familyMember);
}
@@ -378,10 +419,33 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
clientFamilyMember.setRemarks(command.stringValueOfParameterNamed("remarks"));
}
// ClientFamilyMembers
// clientFamilyMembers=ClientFamilyMembers.fromJson(client, firstName,
// middleName, lastName, qualification, relationship, maritalStatus,
// gender, dateOfBirth, profession);
// CBS extended fields
if (command.parameterExists("aadhaarNumber")) {
clientFamilyMember.setAadhaarNumber(command.stringValueOfParameterNamed("aadhaarNumber"));
}
if (command.parameterExists("panNumber")) {
clientFamilyMember.setPanNumber(command.stringValueOfParameterNamed("panNumber"));
}
if (command.parameterExists("nationality")) {
clientFamilyMember.setNationality(command.stringValueOfParameterNamed("nationality"));
}
if (command.parameterExists("isAlive")) {
clientFamilyMember.setIsAlive(command.booleanObjectValueOfParameterNamed("isAlive"));
}
if (command.parameterExists("marriageDate")) {
clientFamilyMember.setMarriageDate(command.localDateValueOfParameterNamed("marriageDate"));
}
if (command.parameterExists("marriageAuthority")) {
clientFamilyMember.setMarriageAuthority(command.stringValueOfParameterNamed("marriageAuthority"));
}
if (command.parameterExists("linkedClientId")) {
final Long linkedId = command.longValueOfParameterNamed("linkedClientId");
if (linkedId != null) {
clientFamilyMember.setLinkedClient(this.clientRepositoryWrapper.findOneWithNotFoundDetection(linkedId));
} else {
clientFamilyMember.setLinkedClient(null);
}
}
this.clientFamilyRepository.saveAndFlush(clientFamilyMember);
@@ -0,0 +1,29 @@
/**
* 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.client.service;
import java.util.List;
import org.apache.fineract.portfolio.client.data.ClientForm60Data;
public interface ClientForm60ReadPlatformService {
List<ClientForm60Data> retrieveAllByClientId(Long clientId);
ClientForm60Data retrieveOne(Long clientId, Long form60Id);
}
@@ -0,0 +1,70 @@
/**
* 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.client.service;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.client.data.ClientForm60Data;
import org.apache.fineract.portfolio.client.domain.ClientForm60;
import org.apache.fineract.portfolio.client.domain.ClientForm60Repository;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ClientForm60ReadPlatformServiceImpl implements ClientForm60ReadPlatformService {
private final ClientForm60Repository form60Repository;
@Override
public List<ClientForm60Data> retrieveAllByClientId(final Long clientId) {
return form60Repository.findByClientId(clientId).stream()
.map(this::toData)
.collect(Collectors.toList());
}
@Override
public ClientForm60Data retrieveOne(final Long clientId, final Long form60Id) {
final ClientForm60 form60 = form60Repository.findByIdAndClientId(form60Id, clientId)
.orElseThrow(() -> new AbstractPlatformResourceNotFoundException("error.msg.client.form60.not.found",
"Form 60 record with identifier " + form60Id + " does not exist", form60Id) {});
return toData(form60);
}
private ClientForm60Data toData(final ClientForm60 f) {
return ClientForm60Data.builder()
.id(f.getId())
.clientId(f.getClient().getId())
.clientDisplayName(f.getClient().getDisplayName())
.fullName(f.getFullName())
.dateOfBirth(f.getDateOfBirth())
.fatherName(f.getFatherName())
.address(f.getAddress())
.transactionType(f.getTransactionType())
.transactionAmount(f.getTransactionAmount())
.estimatedTotalIncome(f.getEstimatedTotalIncome())
.reasonForNoPan(f.getReasonForNoPan())
.declaration(f.isDeclaration())
.submittedOn(f.getSubmittedOn())
.build();
}
}
@@ -0,0 +1,27 @@
/**
* 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.client.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface ClientForm60WritePlatformService {
CommandProcessingResult createForm60(Long clientId, JsonCommand command);
}
@@ -0,0 +1,52 @@
/**
* 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.client.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientForm60;
import org.apache.fineract.portfolio.client.domain.ClientForm60Repository;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ClientForm60WritePlatformServiceImpl implements ClientForm60WritePlatformService {
private final ClientRepositoryWrapper clientRepository;
private final ClientForm60Repository form60Repository;
@Transactional
@Override
public CommandProcessingResult createForm60(final Long clientId, final JsonCommand command) {
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final ClientForm60 form60 = ClientForm60.fromJson(client, command);
form60Repository.saveAndFlush(form60);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(form60.getId())
.build();
}
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.client.service;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.client.data.ClientKycData;
import org.apache.fineract.portfolio.client.domain.ClientKyc;
import org.apache.fineract.portfolio.client.domain.ClientKycRepository;
import org.apache.fineract.portfolio.client.domain.ClientKycRepositoryWrapper;
import org.apache.fineract.portfolio.client.domain.ClientKycStatus;
import org.apache.fineract.portfolio.client.domain.ClientKycType;
@@ -34,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
public class ClientKycReadPlatformServiceImpl implements ClientKycReadPlatformService {
private final ClientKycRepositoryWrapper kycRepositoryWrapper;
private final ClientKycRepository kycRepository;
@Override
public ClientKycData retrieveClientKyc(final Long kycId) {
@@ -43,8 +45,9 @@ public class ClientKycReadPlatformServiceImpl implements ClientKycReadPlatformSe
@Override
public ClientKycData retrieveClientKycByClientId(final Long clientId) {
final ClientKyc kyc = kycRepositoryWrapper.findByClientIdWithNotFoundDetection(clientId);
return toData(kyc);
return kycRepository.findByClientId(clientId)
.map(this::toData)
.orElse(null);
}
private ClientKycData toData(final ClientKyc kyc) {
@@ -66,6 +69,8 @@ public class ClientKycReadPlatformServiceImpl implements ClientKycReadPlatformSe
.panVerified(kyc.isPanVerified())
.panVerifiedOn(kyc.getPanVerifiedOn())
.panFullName(kyc.getPanFullName())
.panFirstName(kyc.getPanFirstName())
.panLastName(kyc.getPanLastName())
.panAadhaarSeedingStatus(kyc.getPanAadhaarSeedingStatus())
.panCategory(kyc.getPanCategory())
.panStatus(kyc.getPanStatus())
@@ -75,6 +80,26 @@ public class ClientKycReadPlatformServiceImpl implements ClientKycReadPlatformSe
.gstinNumber(kyc.getGstinNumber())
.gstinVerified(kyc.isGstinVerified())
.gstinVerifiedOn(kyc.getGstinVerifiedOn())
.enterprisePan(kyc.getEnterprisePan())
.cinNumber(kyc.getCinNumber())
.incorporationDate(kyc.getIncorporationDate())
.kycDocumentName(kyc.getKycDocumentName())
.kycDocumentNumber(kyc.getKycDocumentNumber())
.kycDocIssueDate(kyc.getKycDocIssueDate())
.kycDocExpiryDate(kyc.getKycDocExpiryDate())
.amlCheckStatus(kyc.getAmlCheckStatus())
.bannedEntityMatch(kyc.getBannedEntityMatch())
.willfulDefaulter(kyc.getWillfulDefaulter())
.ecgcSalFlag(kyc.getEcgcSalFlag())
.dormantFlag(kyc.getDormantFlag())
.accountStatus(kyc.getAccountStatus())
.freezeReason(kyc.getFreezeReason())
.freezeAuthority(kyc.getFreezeAuthority())
.form60Type(kyc.getForm60Type())
.form60Date(kyc.getForm60Date())
.form15Applicable(kyc.getForm15Applicable())
.form15Type(kyc.getForm15Type())
.form15Fy(kyc.getForm15Fy())
.ckycNumber(kyc.getCkycNumber())
.initiatedOn(kyc.getInitiatedOn())
.verifiedOn(kyc.getVerifiedOn())
@@ -69,6 +69,16 @@ public interface ClientKycWritePlatformService {
*/
CommandProcessingResult verifyGstin(Long clientId, Long kycId, JsonCommand command);
/**
* Verify CIN (Corporate Identification Number) via LincPay MCA API and mark cinVerified on the KYC record.
*/
CommandProcessingResult verifyCin(Long clientId, Long kycId, JsonCommand command);
/**
* Verify DIN (Director Identification Number) via LincPay MCA API and mark dinVerified on the KYC record.
*/
CommandProcessingResult verifyDin(Long clientId, Long kycId, JsonCommand command);
/** Reject the KYC; stores a rejection reason. */
CommandProcessingResult rejectKyc(Long clientId, Long kycId, JsonCommand command);
}
@@ -31,6 +31,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.command.ClientKycCommand;
import org.apache.fineract.portfolio.client.data.ClientKycApiConstants;
@@ -62,18 +63,26 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
private final ClientKycRepositoryWrapper kycRepositoryWrapper;
private final ClientKycCommandFromApiJsonDeserializer deserializer;
private final KycVerificationService kycVerificationService;
private final FromJsonHelper fromApiJsonHelper;
@Transactional
@Override
public CommandProcessingResult initiateKyc(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
LOG.info("initiateKyc: clientId={}, requestJson={}", clientId, command.json());
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
command.json(), ClientKycApiConstants.INITIATE_REQUEST_PARAMS);
kycCommand.validateForInitiate();
if (kycRepositoryWrapper.existsByClientId(clientId)) {
throw new ClientKycAlreadyExistsException(clientId);
LOG.info("KYC record already exists for client {}. Returning existing record.", clientId);
final ClientKyc existing = kycRepositoryWrapper.findByClientIdWithNotFoundDetection(clientId);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(existing.getId())
.build();
}
try {
@@ -186,12 +195,43 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
if (Boolean.TRUE.equals(kycCommand.getVerifyPan()) && kyc.getPanNumber() != null) {
final String panDob = client.dateOfBirth() != null
? client.dateOfBirth().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) : null;
// Build name without salutation: firstname + middlename (if not blank) + lastname
final StringBuilder panName = new StringBuilder();
if (client.getFirstname() != null && !client.getFirstname().isBlank()) {
panName.append(client.getFirstname().trim());
}
if (client.getMiddlename() != null && !client.getMiddlename().isBlank()) {
panName.append(" ").append(client.getMiddlename().trim());
}
if (client.getLastname() != null && !client.getLastname().isBlank()) {
panName.append(" ").append(client.getLastname().trim());
}
final String clientNameForPan = panName.toString().trim();
LOG.info("verifyPan: using name='{}' (without salutation) for PAN verification", clientNameForPan);
final KycVerificationResult result = kycVerificationService.verifyPan(
kyc.getPanNumber(), client.getDisplayName(), panDob);
kyc.getPanNumber(), clientNameForPan, panDob);
if (result.isSuccess()) {
final JsonObject panData = extractPanData(result.getRawResponse());
kyc.markPanVerified(
getJsonString(panData, "full_name"),
final String fullName = getJsonString(panData, "full_name");
final String firstName = getJsonString(panData, "first_name");
final String lastName = getJsonString(panData, "last_name");
LOG.info("verifyKyc PAN: full_name=[{}], first_name=[{}], last_name=[{}]", fullName, firstName, lastName);
// If LincPay returns empty names, the client's name/DOB does not match the PAN holder
if (fullName == null || fullName.isBlank()) {
LOG.warn("PAN verification name mismatch for client {}: LincPay returned empty full_name. "
+ "Client name '{}' or DOB does not match PAN holder.", clientId, client.getDisplayName());
kyc.markRejected("PAN verification failed: Client name or date of birth does not match the PAN card holder. "
+ "Please ensure the client's name and date of birth exactly match the PAN card details.");
kycRepositoryWrapper.saveAndFlush(kyc);
throw new PlatformServiceUnavailableException(
"error.msg.client.kyc.pan.name.mismatch",
"PAN verification failed: Client name or date of birth does not match the PAN card holder. "
+ "Please ensure the client's name and date of birth exactly match the PAN card details.",
client.getDisplayName());
}
kyc.markPanVerified(fullName, firstName, lastName,
getJsonString(panData, "aadhaar_seeding_status"),
getJsonString(panData, "category"),
getJsonString(panData, "status"));
@@ -266,11 +306,23 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
final String dob = client.dateOfBirth() != null
? client.dateOfBirth().format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
: null;
// Build name without salutation: firstname + middlename (if not blank) + lastname
final StringBuilder panName = new StringBuilder();
if (client.getFirstname() != null && !client.getFirstname().isBlank()) {
panName.append(client.getFirstname().trim());
}
if (client.getMiddlename() != null && !client.getMiddlename().isBlank()) {
panName.append(" ").append(client.getMiddlename().trim());
}
if (client.getLastname() != null && !client.getLastname().isBlank()) {
panName.append(" ").append(client.getLastname().trim());
}
final String clientNameForPan = panName.toString().trim();
LOG.info("verifyPan: calling LincPay with panNumber='{}', name='{}', dob='{}'",
kyc.getPanNumber(), client.getDisplayName(), dob);
kyc.getPanNumber(), clientNameForPan, dob);
final KycVerificationResult result = kycVerificationService.verifyPan(
kyc.getPanNumber(), client.getDisplayName(), dob);
kyc.getPanNumber(), clientNameForPan, dob);
LOG.info("verifyPan: LincPay result success={}, referenceId={}, errorMessage={}, rawResponse={}",
result.isSuccess(), result.getReferenceId(), result.getErrorMessage(), result.getRawResponse());
@@ -285,16 +337,40 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
}
final JsonObject panData = extractPanData(result.getRawResponse());
kyc.markPanVerified(
getJsonString(panData, "full_name"),
final String fullName = getJsonString(panData, "full_name");
// If LincPay returns empty names, the client's name/DOB does not match the PAN holder
if (fullName == null || fullName.isBlank()) {
LOG.error("verifyPan: Name mismatch for clientId={}, kycId={}. Client name '{}' or DOB does not match PAN holder.",
clientId, kycId, client.getDisplayName());
throw new PlatformServiceUnavailableException(
"error.msg.client.kyc.pan.name.mismatch",
"PAN verification failed: Client name or date of birth does not match the PAN card holder. "
+ "Please ensure the client's name and date of birth exactly match the PAN card details.",
client.getDisplayName());
}
kyc.markPanVerified(fullName,
getJsonString(panData, "first_name"),
getJsonString(panData, "last_name"),
getJsonString(panData, "aadhaar_seeding_status"),
getJsonString(panData, "category"),
getJsonString(panData, "status"));
kyc.tryAutoVerify();
kycRepositoryWrapper.saveAndFlush(kyc);
LOG.info("verifyPan: SUCCESS for clientId={}, kycId={}", clientId, kycId);
LOG.info("verifyPan: SUCCESS for clientId={}, kycId={}, kycStatus={}", clientId, kycId, kyc.getKycStatusValue());
return buildResult(command, client, clientId, kycId,
Map.of("panVerified", true, "panVerifiedOn", kyc.getPanVerifiedOn()));
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
changes.put("panVerified", true);
changes.put("panFullName", kyc.getPanFullName() != null ? kyc.getPanFullName() : "");
changes.put("panFirstName", kyc.getPanFirstName() != null ? kyc.getPanFirstName() : "");
changes.put("panLastName", kyc.getPanLastName() != null ? kyc.getPanLastName() : "");
changes.put("panAadhaarSeedingStatus", kyc.getPanAadhaarSeedingStatus() != null ? kyc.getPanAadhaarSeedingStatus() : "");
changes.put("panCategory", kyc.getPanCategory() != null ? kyc.getPanCategory() : "");
changes.put("panStatus", kyc.getPanStatus() != null ? kyc.getPanStatus() : "");
changes.put("panVerifiedOn", kyc.getPanVerifiedOn());
changes.put("kycStatus", kyc.getKycStatusValue());
return buildResult(command, client, clientId, kycId, changes);
}
@Transactional
@@ -324,10 +400,14 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
}
kyc.markAadhaarVerified();
kyc.tryAutoVerify();
kycRepositoryWrapper.saveAndFlush(kyc);
return buildResult(command, client, clientId, kycId,
Map.of("aadhaarVerified", true, "aadhaarVerifiedOn", kyc.getAadhaarVerifiedOn()));
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
changes.put("aadhaarVerified", true);
changes.put("aadhaarVerifiedOn", kyc.getAadhaarVerifiedOn());
changes.put("kycStatus", kyc.getKycStatusValue());
return buildResult(command, client, clientId, kycId, changes);
}
@Transactional
@@ -362,10 +442,98 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
}
kyc.markGstinVerified();
kyc.tryAutoVerify();
kycRepositoryWrapper.saveAndFlush(kyc);
return buildResult(command, client, clientId, kycId,
Map.of("gstinVerified", true, "gstinVerifiedOn", kyc.getGstinVerifiedOn()));
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
changes.put("gstinVerified", true);
changes.put("gstinVerifiedOn", kyc.getGstinVerifiedOn());
changes.put("kycStatus", kyc.getKycStatusValue());
return buildResult(command, client, clientId, kycId, changes);
}
@Transactional
@Override
public CommandProcessingResult verifyCin(final Long clientId, final Long kycId, final JsonCommand command) {
this.context.authenticatedUser();
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
command.json(), ClientKycApiConstants.VERIFY_CIN_REQUEST_PARAMS);
kycCommand.validateForVerifyCin();
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
if (kyc.getKycStatus().isRejected()) {
throw new ClientKycInvalidStateTransitionException("verify-cin", kyc.getKycStatus().name());
}
if (kyc.getCinNumber() == null) {
throw new PlatformServiceUnavailableException(
"error.msg.client.kyc.cin.not.found",
"CIN number is not set on this KYC record. Update the KYC record first.");
}
final KycVerificationResult result = kycVerificationService.verifyCin(kyc.getCinNumber());
if (!result.isSuccess()) {
throw new PlatformServiceUnavailableException(
"error.msg.client.kyc.cin.verification.failed",
"CIN verification failed: " + result.getErrorMessage(),
result.getErrorMessage());
}
kyc.markCinVerified();
kyc.tryAutoVerify();
kycRepositoryWrapper.saveAndFlush(kyc);
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
changes.put("cinVerified", true);
changes.put("cinVerifiedOn", kyc.getCinVerifiedOn());
changes.put("kycStatus", kyc.getKycStatusValue());
return buildResult(command, client, clientId, kycId, changes);
}
@Transactional
@Override
public CommandProcessingResult verifyDin(final Long clientId, final Long kycId, final JsonCommand command) {
this.context.authenticatedUser();
final ClientKycCommand kycCommand = deserializer.commandFromApiJson(
command.json(), ClientKycApiConstants.VERIFY_DIN_REQUEST_PARAMS);
kycCommand.validateForVerifyDin();
final Client client = clientRepository.findOneWithNotFoundDetection(clientId);
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
if (kyc.getKycStatus().isRejected()) {
throw new ClientKycInvalidStateTransitionException("verify-din", kyc.getKycStatus().name());
}
if (kyc.getDinNumber() == null) {
throw new PlatformServiceUnavailableException(
"error.msg.client.kyc.din.not.found",
"DIN number is not set on this KYC record. Update the KYC record first.");
}
final KycVerificationResult result = kycVerificationService.verifyDin(kyc.getDinNumber());
if (!result.isSuccess()) {
throw new PlatformServiceUnavailableException(
"error.msg.client.kyc.din.verification.failed",
"DIN verification failed: " + result.getErrorMessage(),
result.getErrorMessage());
}
kyc.markDinVerified();
kyc.tryAutoVerify();
kycRepositoryWrapper.saveAndFlush(kyc);
final Map<String, Object> changes = new java.util.LinkedHashMap<>();
changes.put("dinVerified", true);
changes.put("dinVerifiedOn", kyc.getDinVerifiedOn());
changes.put("kycStatus", kyc.getKycStatusValue());
return buildResult(command, client, clientId, kycId, changes);
}
@Transactional
@@ -398,13 +566,14 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
kycCommand.validateForInitiate();
if (kycRepositoryWrapper.existsByClientId(client.getId())) {
throw new ClientKycAlreadyExistsException(client.getId());
LOG.info("KYC record already exists for client {}. Skipping inline initiation.", client.getId());
return;
}
final ClientKyc kyc = ClientKyc.initiateKyc(client,
kycCommand.getKycType(), kycCommand.getRiskCategory(),
kycCommand.getPanNumber(), kycCommand.getAadhaarNumberMasked(),
kycCommand.getGstinNumber(), kycCommand.getRemarks());
final JsonElement parsedJson = this.fromApiJsonHelper.parse(kycJson);
final JsonCommand jsonCommand = JsonCommand.from(kycJson, parsedJson, this.fromApiJsonHelper,
"CLIENTKYC", null, null, null, client.getId(), null, null, null, null, null, null, null, null, null);
final ClientKyc kyc = ClientKyc.initiateKyc(client, jsonCommand);
kycRepositoryWrapper.saveAndFlush(kyc);
}
@@ -453,7 +622,8 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
&& cause.getMessage().contains("uq_m_client_kyc_client_id")) {
throw new ClientKycAlreadyExistsException(-1L);
}
LOG.error("Unexpected data integrity issue in KYC.", dve);
LOG.error("Unexpected data integrity issue in KYC. Root cause: {}",
cause != null ? cause.getMessage() : "unknown", dve);
throw ErrorHandler.getMappable(dve, "error.msg.client.kyc.unknown.data.integrity.issue",
"Unknown data integrity issue with KYC resource.");
}
@@ -0,0 +1,36 @@
/**
* 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.client.service;
import java.util.List;
import org.apache.fineract.portfolio.client.data.ClientNomineeData;
public interface ClientNomineeReadPlatformService {
List<ClientNomineeData> retrieveAllNominees(Long clientId);
List<ClientNomineeData> retrieveNomineesByAccount(Long clientId, String accountNo, String accountType);
ClientNomineeData retrieveNominee(Long clientId, Long nomineeId);
ClientNomineeData retrieveTemplate(Long clientId);
/** Returns true if the client has at least one savings, current, FD, RD, or loan account */
boolean clientHasAnyAccount(Long clientId);
}
@@ -0,0 +1,127 @@
/**
* 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.client.service;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.portfolio.client.data.ClientNomineeData;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientNominee;
import org.apache.fineract.portfolio.client.domain.ClientNomineeRepository;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ClientNomineeReadPlatformServiceImpl implements ClientNomineeReadPlatformService {
private final ClientNomineeRepository nomineeRepository;
private final CodeValueReadPlatformService codeValueReadPlatformService;
private final SavingsAccountRepository savingsAccountRepository;
private final LoanRepository loanRepository;
@Override
public List<ClientNomineeData> retrieveAllNominees(final Long clientId) {
final List<ClientNominee> nominees = nomineeRepository.findAllByClientId(clientId);
return nominees.stream().map(this::toData).collect(Collectors.toList());
}
@Override
public List<ClientNomineeData> retrieveNomineesByAccount(final Long clientId, final String accountNo, final String accountType) {
final List<ClientNominee> nominees = nomineeRepository.findByClientIdAndAccountNoAndAccountType(clientId, accountNo, accountType);
return nominees.stream().map(this::toData).collect(Collectors.toList());
}
@Override
public ClientNomineeData retrieveNominee(final Long clientId, final Long nomineeId) {
final ClientNominee nominee = nomineeRepository.findByClientIdAndId(clientId, nomineeId)
.orElseThrow(() -> new ClientNotFoundException(nomineeId));
return toData(nominee);
}
@Override
public ClientNomineeData retrieveTemplate(final Long clientId) {
final ClientNomineeData data = new ClientNomineeData();
data.setClientId(clientId);
final Collection<CodeValueData> relationshipOptions = codeValueReadPlatformService
.retrieveCodeValuesByCode("RELATIONSHIP");
data.setRelationshipOptions(relationshipOptions);
return data;
}
@Override
public boolean clientHasAnyAccount(final Long clientId) {
// Check savings/current/FD/RD accounts
final List<?> savingsAccounts = savingsAccountRepository.findSavingAccountByClientId(clientId);
if (savingsAccounts != null && !savingsAccounts.isEmpty()) {
return true;
}
// Check loan accounts
final List<?> loans = loanRepository.findLoanByClientId(clientId);
return loans != null && !loans.isEmpty();
}
private ClientNomineeData toData(final ClientNominee n) {
final ClientNomineeData d = new ClientNomineeData();
d.setId(n.getId());
d.setClientId(n.getClient().getId());
final Client nominee = n.getNomineeClient();
d.setNomineeClientId(nominee.getId());
d.setNomineeDisplayName(nominee.getDisplayName());
d.setNomineeCifNumber(nominee.getCifNumber());
d.setNomineeAccountNo(nominee.getAccountNumber());
d.setNomineeDateOfBirth(nominee.getDateOfBirth());
d.setNomineeMobileNo(nominee.getMobileNo());
if (n.getRelationship() != null) {
d.setRelationshipId(n.getRelationship().getId());
d.setRelationshipName(n.getRelationship().getLabel());
}
d.setPercentageShare(n.getPercentageShare());
d.setIsActive(n.getIsActive());
d.setNomineeIsMinor(n.getNomineeIsMinor());
if (n.getGuardianClient() != null) {
d.setGuardianClientId(n.getGuardianClient().getId());
d.setGuardianDisplayName(n.getGuardianClient().getDisplayName());
d.setGuardianCifNumber(n.getGuardianClient().getCifNumber());
}
if (n.getGuardianRelationship() != null) {
d.setGuardianRelationshipId(n.getGuardianRelationship().getId());
d.setGuardianRelationshipName(n.getGuardianRelationship().getLabel());
}
d.setEffectiveDate(n.getEffectiveDate());
d.setRemarks(n.getRemarks());
d.setNomineeIdentifier(n.getNomineeIdentifier());
d.setAccountNo(n.getAccountNo());
d.setAccountType(n.getAccountType());
return d;
}
}
@@ -0,0 +1,31 @@
/**
* 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.client.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface ClientNomineeWritePlatformService {
CommandProcessingResult addNominee(Long clientId, JsonCommand command);
CommandProcessingResult updateNominee(Long clientId, Long nomineeId, JsonCommand command);
CommandProcessingResult deleteNominee(Long clientId, Long nomineeId);
}
@@ -0,0 +1,299 @@
/**
* 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.client.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientNominee;
import org.apache.fineract.portfolio.client.domain.ClientNomineeRepository;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotFoundException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ClientNomineeWritePlatformServiceImpl implements ClientNomineeWritePlatformService {
private static final String RELATIONSHIP_CODE = "RELATIONSHIP";
private static final Set<String> VALID_ACCOUNT_TYPES = Set.of("SB", "CB", "FD", "RD", "LA");
private final PlatformSecurityContext context;
private final ClientRepositoryWrapper clientRepository;
private final ClientNomineeRepository nomineeRepository;
private final CodeValueRepositoryWrapper codeValueRepository;
private final SavingsAccountRepository savingsAccountRepository;
private final LoanRepository loanRepository;
@Override
@Transactional
public CommandProcessingResult addNominee(final Long clientId, final JsonCommand command) {
this.context.authenticatedUser();
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
// ── Account linkage (MANDATORY) ──
final String accountNo = command.stringValueOfParameterNamed("accountNo");
final String accountType = command.stringValueOfParameterNamed("accountType");
validateAccountParams(accountNo, accountType);
validateAccountBelongsToClient(clientId, accountNo, accountType);
// ── Nominee client ──
final Long nomineeClientId = command.longValueOfParameterNamed("nomineeClientId");
if (nomineeClientId == null) {
throw new PlatformDataIntegrityException("error.msg.nominee.client.id.required",
"Nominee must be an existing client. nomineeClientId is required.", "nomineeClientId");
}
if (nomineeClientId.equals(clientId)) {
throw new PlatformDataIntegrityException("error.msg.nominee.cannot.be.self",
"Client cannot be their own nominee.", "nomineeClientId");
}
final Client nomineeClient = this.clientRepository.findOneWithNotFoundDetection(nomineeClientId);
// ── Percentage share validation (per account, not per client) ──
final BigDecimal newShare = command.bigDecimalValueOfParameterNamed("percentageShare");
validatePercentageShareForAccount(accountNo, accountType, null, newShare);
final ClientNominee nominee = new ClientNominee();
nominee.setClient(client);
nominee.setNomineeClient(nomineeClient);
nominee.setPercentageShare(newShare);
nominee.setIsActive(command.booleanObjectValueOfParameterNamed("isActive") != null
? command.booleanObjectValueOfParameterNamed("isActive") : true);
nominee.setAccountNo(accountNo);
nominee.setAccountType(accountType);
nominee.setNomineeIdentifier(generateNomineeIdentifier(accountNo, accountType));
final Long relationshipId = command.longValueOfParameterNamed("relationshipId");
if (relationshipId != null) {
final CodeValue relationship = codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(
RELATIONSHIP_CODE, relationshipId);
nominee.setRelationship(relationship);
}
final Boolean nomineeIsMinor = command.booleanObjectValueOfParameterNamed("nomineeIsMinor");
nominee.setNomineeIsMinor(nomineeIsMinor != null ? nomineeIsMinor : false);
if (Boolean.TRUE.equals(nomineeIsMinor)) {
final Long guardianClientId = command.longValueOfParameterNamed("guardianClientId");
if (guardianClientId != null) {
final Client guardian = this.clientRepository.findOneWithNotFoundDetection(guardianClientId);
nominee.setGuardianClient(guardian);
}
final Long guardianRelationshipId = command.longValueOfParameterNamed("guardianRelationshipId");
if (guardianRelationshipId != null) {
final CodeValue guardianRel = codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(
RELATIONSHIP_CODE, guardianRelationshipId);
nominee.setGuardianRelationship(guardianRel);
}
}
nominee.setEffectiveDate(command.localDateValueOfParameterNamed("effectiveDate"));
nominee.setRemarks(command.stringValueOfParameterNamed("remarks"));
nomineeRepository.saveAndFlush(nominee);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(nominee.getId())
.build();
}
@Override
@Transactional
public CommandProcessingResult updateNominee(final Long clientId, final Long nomineeId, final JsonCommand command) {
this.context.authenticatedUser();
final ClientNominee nominee = nomineeRepository.findByClientIdAndId(clientId, nomineeId)
.orElseThrow(() -> new ClientNotFoundException(nomineeId));
final Long nomineeClientId = command.longValueOfParameterNamed("nomineeClientId");
if (nomineeClientId != null) {
if (nomineeClientId.equals(clientId)) {
throw new PlatformDataIntegrityException("error.msg.nominee.cannot.be.self",
"Client cannot be their own nominee.", "nomineeClientId");
}
final Client nomineeClient = this.clientRepository.findOneWithNotFoundDetection(nomineeClientId);
nominee.setNomineeClient(nomineeClient);
}
// Percentage share validated per account
final BigDecimal newShare = command.bigDecimalValueOfParameterNamed("percentageShare");
if (newShare != null) {
validatePercentageShareForAccount(nominee.getAccountNo(), nominee.getAccountType(), nomineeId, newShare);
nominee.setPercentageShare(newShare);
}
final Long relationshipId = command.longValueOfParameterNamed("relationshipId");
if (relationshipId != null) {
final CodeValue relationship = codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(
RELATIONSHIP_CODE, relationshipId);
nominee.setRelationship(relationship);
}
if (command.parameterExists("isActive")) {
nominee.setIsActive(command.booleanObjectValueOfParameterNamed("isActive"));
}
if (command.parameterExists("nomineeIsMinor")) {
final Boolean isMinor = command.booleanObjectValueOfParameterNamed("nomineeIsMinor");
nominee.setNomineeIsMinor(isMinor);
if (Boolean.TRUE.equals(isMinor)) {
final Long guardianClientId = command.longValueOfParameterNamed("guardianClientId");
if (guardianClientId != null) {
nominee.setGuardianClient(this.clientRepository.findOneWithNotFoundDetection(guardianClientId));
}
final Long guardianRelationshipId = command.longValueOfParameterNamed("guardianRelationshipId");
if (guardianRelationshipId != null) {
nominee.setGuardianRelationship(codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(
RELATIONSHIP_CODE, guardianRelationshipId));
}
} else {
nominee.setGuardianClient(null);
nominee.setGuardianRelationship(null);
}
}
if (command.parameterExists("effectiveDate")) {
nominee.setEffectiveDate(command.localDateValueOfParameterNamed("effectiveDate"));
}
if (command.parameterExists("remarks")) {
nominee.setRemarks(command.stringValueOfParameterNamed("remarks"));
}
nomineeRepository.saveAndFlush(nominee);
return new CommandProcessingResultBuilder()
.withCommandId(command.commandId())
.withClientId(clientId)
.withEntityId(nominee.getId())
.build();
}
@Override
@Transactional
public CommandProcessingResult deleteNominee(final Long clientId, final Long nomineeId) {
this.context.authenticatedUser();
final ClientNominee nominee = nomineeRepository.findByClientIdAndId(clientId, nomineeId)
.orElseThrow(() -> new ClientNotFoundException(nomineeId));
nomineeRepository.delete(nominee);
return new CommandProcessingResultBuilder()
.withClientId(clientId)
.withEntityId(nomineeId)
.build();
}
// ── Validation helpers ──────────────────────────────────────────────────
private void validateAccountParams(final String accountNo, final String accountType) {
if (accountNo == null || accountNo.isBlank()) {
throw new PlatformDataIntegrityException("error.msg.nominee.account.no.required",
"accountNo is required. Nominees must be linked to a specific account.", "accountNo");
}
if (accountType == null || accountType.isBlank()) {
throw new PlatformDataIntegrityException("error.msg.nominee.account.type.required",
"accountType is required. Must be one of: SB, CB, FD, RD, LA.", "accountType");
}
if (!VALID_ACCOUNT_TYPES.contains(accountType)) {
throw new PlatformDataIntegrityException("error.msg.nominee.account.type.invalid",
"Invalid accountType '" + accountType + "'. Must be one of: SB, CB, FD, RD, LA.", "accountType");
}
}
/**
* Validates that the account exists and belongs to the client.
* SB/CB/FD/RD → savings account; LA → loan account.
*/
private void validateAccountBelongsToClient(final Long clientId, final String accountNo, final String accountType) {
if ("LA".equals(accountType)) {
final List<Loan> clientLoans = loanRepository.findLoanByClientId(clientId);
final boolean found = clientLoans.stream()
.anyMatch(loan -> accountNo.equals(loan.getAccountNumber()));
if (!found) {
throw new PlatformDataIntegrityException("error.msg.nominee.loan.account.not.found",
"Loan account '" + accountNo + "' not found for client " + clientId + ".", "accountNo");
}
} else {
// SB, CB, FD, RD are all savings accounts with different deposit_type_enum
final SavingsAccount sa = savingsAccountRepository.findSavingsAccountByAccountNumber(accountNo);
if (sa == null) {
throw new PlatformDataIntegrityException("error.msg.nominee.savings.account.not.found",
"Account '" + accountNo + "' not found.", "accountNo");
}
if (sa.getClient() == null || !sa.getClient().getId().equals(clientId)) {
throw new PlatformDataIntegrityException("error.msg.nominee.account.does.not.belong.to.client",
"Account '" + accountNo + "' does not belong to client " + clientId + ".", "accountNo");
}
}
}
/**
* Generates a unique nominee identifier: NOM-{TYPE}{ACCNO}-{SEQ}
* e.g. NOM-SB00123456-01, NOM-CB00123456-02, NOM-LA00123456-01
*/
private String generateNomineeIdentifier(final String accountNo, final String accountType) {
final long existingCount = nomineeRepository.countByAccountNoAndAccountType(accountNo, accountType);
final long seq = existingCount + 1;
return String.format("NOM-%s%s-%02d", accountType, accountNo, seq);
}
/**
* Validates that total percentage share for an account does not exceed 100%.
* This is per-account validation, not per-client.
*/
private void validatePercentageShareForAccount(final String accountNo, final String accountType,
final Long excludeNomineeId, final BigDecimal newShare) {
if (newShare == null) {
return;
}
final List<ClientNominee> existing = nomineeRepository.findActiveByAccountNoAndAccountType(accountNo, accountType);
BigDecimal total = newShare;
for (final ClientNominee n : existing) {
if (excludeNomineeId != null && excludeNomineeId.equals(n.getId())) {
continue;
}
if (n.getPercentageShare() != null) {
total = total.add(n.getPercentageShare());
}
}
if (total.compareTo(new BigDecimal("100")) > 0) {
throw new PlatformDataIntegrityException("error.msg.nominee.percentage.exceeds.100",
"Total nominee percentage share for account " + accountType + accountNo
+ " cannot exceed 100%. Current total would be " + total + "%.",
"percentageShare", total);
}
}
}
@@ -22,6 +22,7 @@ import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -308,6 +309,8 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
sqlBuilder.append("c.date_of_birth as dateOfBirth, ");
sqlBuilder.append("c.gender_cv_id as genderId, ");
sqlBuilder.append("cv.code_value as genderValue, ");
sqlBuilder.append("c.salutation_cv_id as salutationId, ");
sqlBuilder.append("cvSalutation.code_value as salutationValue, ");
sqlBuilder.append("c.client_type_cv_id as clienttypeId, ");
sqlBuilder.append("cvclienttype.code_value as clienttypeValue, ");
sqlBuilder.append("c.client_classification_cv_id as classificationId, ");
@@ -317,6 +320,22 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
sqlBuilder.append("cvOccupationCategory.code_value as occupationCategoryValue, ");
sqlBuilder.append("c.occupation_cv_id as occupationId, ");
sqlBuilder.append("cvOccupation.code_value as occupationValue, ");
sqlBuilder.append("c.monthly_income as monthlyIncome, ");
sqlBuilder.append("c.annual_turnover as annualTurnover, ");
sqlBuilder.append("c.nationality as nationality, ");
sqlBuilder.append("c.is_minor as isMinor, ");
sqlBuilder.append("c.guardian_client_id as guardianClientId, ");
sqlBuilder.append("gc.display_name as guardianClientDisplayName, ");
sqlBuilder.append("c.guardian_relation as guardianRelation, ");
sqlBuilder.append("c.alternate_mobile_no as alternateMobileNo, ");
sqlBuilder.append("c.dip_verified as dipVerified, ");
sqlBuilder.append("c.dip_lookup_log_id as dipLookupLogId, ");
sqlBuilder.append("c.dip_timestamp as dipTimestamp, ");
sqlBuilder.append("c.alternate_dip_verified as alternateDipVerified, ");
sqlBuilder.append("c.alternate_dip_lookup_log_id as alternateDipLookupLogId, ");
sqlBuilder.append("c.alternate_dip_timestamp as alternateDipTimestamp, ");
sqlBuilder.append("c.name_verified as nameVerified, ");
sqlBuilder.append("c.screening_log_id as screeningLogId, ");
sqlBuilder.append("c.activation_date as activationDate, c.image_id as imageId, ");
sqlBuilder.append("c.staff_id as staffId, s.display_name as staffName,");
sqlBuilder.append("c.default_savings_product as savingsProductId, sp.name as savingsProductName, ");
@@ -342,7 +361,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
sqlBuilder.append("cnp.incorp_validity_till as incorpValidityTill, ");
sqlBuilder.append("cnp.main_business_line_cv_id as mainBusinessLineId, ");
sqlBuilder.append("cvMainBusinessLine.code_value as mainBusinessLineValue, ");
sqlBuilder.append("cnp.remarks as remarks ");
sqlBuilder.append("cnp.remarks as remarks, ");
sqlBuilder.append("cnp.authorized_signatory as authorizedSignatory, ");
sqlBuilder.append("cnp.additional_info as additionalInfo, ");
sqlBuilder.append("cnp.date_of_registration as dateOfRegistration ");
sqlBuilder.append("from m_client c ");
sqlBuilder.append("join m_office o on o.id = c.office_id ");
@@ -356,11 +378,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
sqlBuilder.append("left join m_appuser acu on acu.id = c.activatedon_userid ");
sqlBuilder.append("left join m_appuser clu on clu.id = c.closedon_userid ");
sqlBuilder.append("left join m_code_value cv on cv.id = c.gender_cv_id ");
sqlBuilder.append("left join m_code_value cvSalutation on cvSalutation.id = c.salutation_cv_id ");
sqlBuilder.append("left join m_code_value cvclienttype on cvclienttype.id = c.client_type_cv_id ");
sqlBuilder.append("left join m_code_value cvclassification on cvclassification.id = c.client_classification_cv_id ");
sqlBuilder.append("left join m_code_value cvSubStatus on cvSubStatus.id = c.sub_status ");
sqlBuilder.append("left join m_code_value cvOccupationCategory on cvOccupationCategory.id = c.occupation_category_cv_id ");
sqlBuilder.append("left join m_code_value cvOccupation on cvOccupation.id = c.occupation_cv_id ");
sqlBuilder.append("left join m_client gc on gc.id = c.guardian_client_id ");
sqlBuilder.append("left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id ");
sqlBuilder.append("left join m_code_value cvMainBusinessLine on cvMainBusinessLine.id = cnp.main_business_line_cv_id ");
@@ -407,6 +431,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String genderValue = rs.getString("genderValue");
final CodeValueData gender = CodeValueData.instance(genderId, genderValue);
final Long salutationId = JdbcSupport.getLong(rs, "salutationId");
final String salutationValue = rs.getString("salutationValue");
final CodeValueData salutation = CodeValueData.instance(salutationId, salutationValue);
final Long clienttypeId = JdbcSupport.getLong(rs, "clienttypeId");
final String clienttypeValue = rs.getString("clienttypeValue");
final CodeValueData clienttype = CodeValueData.instance(clienttypeId, clienttypeValue);
@@ -453,6 +481,19 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String occupationValue = rs.getString("occupationValue");
final CodeValueData occupation = CodeValueData.instance(occupationId, occupationValue);
final BigDecimal monthlyIncome1 = rs.getBigDecimal("monthlyIncome");
final BigDecimal annualTurnover1 = rs.getBigDecimal("annualTurnover");
Integer ageYears1 = null;
Integer ageMonths1 = null;
Integer ageDays1 = null;
if (dateOfBirth != null) {
final Period age = Period.between(dateOfBirth, LocalDate.now());
ageYears1 = age.getYears();
ageMonths1 = age.getMonths();
ageDays1 = age.getDays();
}
final Long constitutionId = JdbcSupport.getLong(rs, "constitutionId");
final String constitutionValue = rs.getString("constitutionValue");
final CodeValueData constitution = CodeValueData.instance(constitutionId, constitutionValue);
@@ -462,18 +503,42 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String mainBusinessLineValue = rs.getString("mainBusinessLineValue");
final CodeValueData mainBusinessLine = CodeValueData.instance(mainBusinessLineId, mainBusinessLineValue);
final String remarks = rs.getString("remarks");
final String authorizedSignatory = rs.getString("authorizedSignatory");
final String additionalInfo = rs.getString("additionalInfo");
final LocalDate dateOfRegistration = JdbcSupport.getLocalDate(rs, "dateOfRegistration");
final ClientNonPersonData clientNonPerson = new ClientNonPersonData(constitution, incorpNo, incorpValidityTill,
mainBusinessLine, remarks);
mainBusinessLine, remarks, authorizedSignatory, additionalInfo, dateOfRegistration);
final ClientTimelineData timeline = new ClientTimelineData(submittedOnDate, submittedByUsername, submittedByFirstname,
submittedByLastname, activationDate, activatedByUsername, activatedByFirstname, activatedByLastname, closedOnDate,
closedByUsername, closedByFirstname, closedByLastname);
return ClientData.instance(accountNo, cifNumber, status, subStatus, officeId, officeName, transferToOfficeId,
ClientData clientData = ClientData.instance(accountNo, cifNumber, status, subStatus, officeId, officeName, transferToOfficeId,
transferToOfficeName, id, firstname, middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress,
dateOfBirth, gender, activationDate, imageId, staffId, staffName, timeline, savingsProductId, savingsProductName,
savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm, clientNonPerson, isStaff);
dateOfBirth, gender, salutation, activationDate, imageId, staffId, staffName, timeline, savingsProductId,
savingsProductName, savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm,
clientNonPerson, isStaff);
clientData.setMonthlyIncome(monthlyIncome1);
clientData.setAnnualTurnover(annualTurnover1);
clientData.setAgeYears(ageYears1);
clientData.setAgeMonths(ageMonths1);
clientData.setAgeDays(ageDays1);
clientData.setNationality(rs.getString("nationality"));
clientData.setIsMinor(rs.getBoolean("isMinor"));
clientData.setGuardianClientId(JdbcSupport.getLong(rs, "guardianClientId"));
clientData.setGuardianClientDisplayName(rs.getString("guardianClientDisplayName"));
clientData.setGuardianRelation(rs.getString("guardianRelation"));
clientData.setAlternateMobileNo(rs.getString("alternateMobileNo"));
clientData.setDipVerified(rs.getBoolean("dipVerified"));
clientData.setDipLookupLogId(JdbcSupport.getLong(rs, "dipLookupLogId"));
clientData.setDipTimestamp(rs.getString("dipTimestamp"));
clientData.setAlternateDipVerified(rs.getBoolean("alternateDipVerified"));
clientData.setAlternateDipLookupLogId(JdbcSupport.getLong(rs, "alternateDipLookupLogId"));
clientData.setAlternateDipTimestamp(rs.getString("alternateDipTimestamp"));
clientData.setNameVerified(rs.getBoolean("nameVerified"));
clientData.setScreeningLogId(JdbcSupport.getLong(rs, "screeningLogId"));
return clientData;
}
}
@@ -591,7 +656,7 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final Collection<CodeValueData> clientNonPersonConstitutionOptions = null;
final Collection<CodeValueData> clientNonPersonMainBusinessLineOptions = null;
final List<EnumOptionData> clientLegalFormOptions = null;
return ClientData.template(null, null, null, null, narrations, null, null, clientTypeOptions, clientClassificationOptions,
return ClientData.template(null, null, null, null, narrations, null, null, null, clientTypeOptions, clientClassificationOptions,
null, null, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions, clientLegalFormOptions, null, null,
null, null);
}
@@ -637,6 +702,8 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("c.date_of_birth as dateOfBirth, ");
builder.append("c.gender_cv_id as genderId, ");
builder.append("cv.code_value as genderValue, ");
builder.append("c.salutation_cv_id as salutationId, ");
builder.append("cvSalutation.code_value as salutationValue, ");
builder.append("c.client_type_cv_id as clienttypeId, ");
builder.append("cvclienttype.code_value as clienttypeValue, ");
builder.append("c.client_classification_cv_id as classificationId, ");
@@ -646,6 +713,18 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("cvOccupationCategory.code_value as occupationCategoryValue, ");
builder.append("c.occupation_cv_id as occupationId, ");
builder.append("cvOccupation.code_value as occupationValue, ");
builder.append("c.monthly_income as monthlyIncome, ");
builder.append("c.annual_turnover as annualTurnover, ");
builder.append("c.nationality as nationality, ");
builder.append("c.is_minor as isMinor, ");
builder.append("c.guardian_client_id as guardianClientId, ");
builder.append("c.guardian_relation as guardianRelation, ");
builder.append("c.alternate_mobile_no as alternateMobileNo, ");
builder.append("c.dip_verified as dipVerified, ");
builder.append("c.alternate_dip_verified as alternateDipVerified, ");
builder.append("c.alternate_dip_lookup_log_id as alternateDipLookupLogId, ");
builder.append("c.alternate_dip_timestamp as alternateDipTimestamp, ");
builder.append("c.name_verified as nameVerified, ");
builder.append("c.submittedon_date as submittedOnDate, ");
builder.append("sbu.username as submittedByUsername, ");
@@ -669,6 +748,9 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("cnp.main_business_line_cv_id as mainBusinessLineId, ");
builder.append("cvMainBusinessLine.code_value as mainBusinessLineValue, ");
builder.append("cnp.remarks as remarks, ");
builder.append("cnp.authorized_signatory as authorizedSignatory, ");
builder.append("cnp.additional_info as additionalInfo, ");
builder.append("cnp.date_of_registration as dateOfRegistration, ");
builder.append("c.activation_date as activationDate, c.image_id as imageId, ");
builder.append("c.staff_id as staffId, s.display_name as staffName, ");
@@ -684,11 +766,13 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("left join m_appuser acu on acu.id = c.activatedon_userid ");
builder.append("left join m_appuser clu on clu.id = c.closedon_userid ");
builder.append("left join m_code_value cv on cv.id = c.gender_cv_id ");
builder.append("left join m_code_value cvSalutation on cvSalutation.id = c.salutation_cv_id ");
builder.append("left join m_code_value cvclienttype on cvclienttype.id = c.client_type_cv_id ");
builder.append("left join m_code_value cvclassification on cvclassification.id = c.client_classification_cv_id ");
builder.append("left join m_code_value cvSubStatus on cvSubStatus.id = c.sub_status ");
builder.append("left join m_code_value cvOccupationCategory on cvOccupationCategory.id = c.occupation_category_cv_id ");
builder.append("left join m_code_value cvOccupation on cvOccupation.id = c.occupation_cv_id ");
builder.append("left join m_client gc on gc.id = c.guardian_client_id ");
builder.append("left join m_code_value cvConstitution on cvConstitution.id = cnp.constitution_cv_id ");
builder.append("left join m_code_value cvMainBusinessLine on cvMainBusinessLine.id = cnp.main_business_line_cv_id ");
@@ -735,6 +819,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String genderValue = rs.getString("genderValue");
final CodeValueData gender = CodeValueData.instance(genderId, genderValue);
final Long salutationId = JdbcSupport.getLong(rs, "salutationId");
final String salutationValue = rs.getString("salutationValue");
final CodeValueData salutation = CodeValueData.instance(salutationId, salutationValue);
final Long clienttypeId = JdbcSupport.getLong(rs, "clienttypeId");
final String clienttypeValue = rs.getString("clienttypeValue");
final CodeValueData clienttype = CodeValueData.instance(clienttypeId, clienttypeValue);
@@ -780,6 +868,19 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String occupationValue = rs.getString("occupationValue");
final CodeValueData occupation = CodeValueData.instance(occupationId, occupationValue);
final BigDecimal monthlyIncome = rs.getBigDecimal("monthlyIncome");
final BigDecimal annualTurnover = rs.getBigDecimal("annualTurnover");
Integer ageYears = null;
Integer ageMonths = null;
Integer ageDays = null;
if (dateOfBirth != null) {
final Period age = Period.between(dateOfBirth, LocalDate.now());
ageYears = age.getYears();
ageMonths = age.getMonths();
ageDays = age.getDays();
}
final Long constitutionId = JdbcSupport.getLong(rs, "constitutionId");
final String constitutionValue = rs.getString("constitutionValue");
final CodeValueData constitution = CodeValueData.instance(constitutionId, constitutionValue);
@@ -789,18 +890,42 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
final String mainBusinessLineValue = rs.getString("mainBusinessLineValue");
final CodeValueData mainBusinessLine = CodeValueData.instance(mainBusinessLineId, mainBusinessLineValue);
final String remarks = rs.getString("remarks");
final String authorizedSignatory = rs.getString("authorizedSignatory");
final String additionalInfo = rs.getString("additionalInfo");
final LocalDate dateOfRegistration = JdbcSupport.getLocalDate(rs, "dateOfRegistration");
final ClientNonPersonData clientNonPerson = new ClientNonPersonData(constitution, incorpNo, incorpValidityTill,
mainBusinessLine, remarks);
mainBusinessLine, remarks, authorizedSignatory, additionalInfo, dateOfRegistration);
final ClientTimelineData timeline = new ClientTimelineData(submittedOnDate, submittedByUsername, submittedByFirstname,
submittedByLastname, activationDate, activatedByUsername, activatedByFirstname, activatedByLastname, closedOnDate,
closedByUsername, closedByFirstname, closedByLastname);
return ClientData.instance(accountNo, cifNumber, status, subStatus, officeId, officeName, transferToOfficeId,
ClientData clientData = ClientData.instance(accountNo, cifNumber, status, subStatus, officeId, officeName, transferToOfficeId,
transferToOfficeName, id, firstname, middlename, lastname, fullname, displayName, externalId, mobileNo, emailAddress,
dateOfBirth, gender, activationDate, imageId, staffId, staffName, timeline, savingsProductId, savingsProductName,
savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm, clientNonPerson, isStaff);
dateOfBirth, gender, salutation, activationDate, imageId, staffId, staffName, timeline, savingsProductId,
savingsProductName, savingsAccountId, clienttype, classification, occupationCategory, occupation, legalForm,
clientNonPerson, isStaff);
clientData.setMonthlyIncome(monthlyIncome);
clientData.setAnnualTurnover(annualTurnover);
clientData.setAgeYears(ageYears);
clientData.setAgeMonths(ageMonths);
clientData.setAgeDays(ageDays);
clientData.setNationality(rs.getString("nationality"));
clientData.setIsMinor(rs.getBoolean("isMinor"));
clientData.setGuardianClientId(JdbcSupport.getLong(rs, "guardianClientId"));
clientData.setGuardianClientDisplayName(rs.getString("guardianClientDisplayName"));
clientData.setGuardianRelation(rs.getString("guardianRelation"));
clientData.setAlternateMobileNo(rs.getString("alternateMobileNo"));
clientData.setDipVerified(rs.getBoolean("dipVerified"));
clientData.setDipLookupLogId(JdbcSupport.getLong(rs, "dipLookupLogId"));
clientData.setDipTimestamp(rs.getString("dipTimestamp"));
clientData.setAlternateDipVerified(rs.getBoolean("alternateDipVerified"));
clientData.setAlternateDipLookupLogId(JdbcSupport.getLong(rs, "alternateDipLookupLogId"));
clientData.setAlternateDipTimestamp(rs.getString("alternateDipTimestamp"));
clientData.setNameVerified(rs.getBoolean("nameVerified"));
clientData.setScreeningLogId(JdbcSupport.getLong(rs, "screeningLogId"));
return clientData;
}
}
@@ -99,6 +99,9 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead
final List<CodeValueData> genderOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.GENDER));
final List<CodeValueData> salutationOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.SALUTATION));
final List<CodeValueData> clientTypeOptions = new ArrayList<>(
this.codeValueReadPlatformService.retrieveCodeValuesByCode(ClientApiConstants.CLIENT_TYPE));
@@ -123,9 +126,10 @@ public class ClientTemplateReadPlatformServiceImpl implements ClientTemplateRead
EntityTables.CLIENT.getName(), null);
return ClientData.template(defaultOfficeId, LocalDate.now(DateUtils.getDateTimeZoneOfTenant()), offices, staffOptions, null,
genderOptions, savingsProductDatas, clientTypeOptions, clientClassificationOptions, occupationCategoryOptions,
occupationOptions, clientNonPersonConstitutionOptions, clientNonPersonMainBusinessLineOptions, clientLegalFormOptions,
familyMemberOptions, new ArrayList<AddressData>(Arrays.asList(address)), isAddressEnabled, datatableTemplates);
genderOptions, salutationOptions, savingsProductDatas, clientTypeOptions, clientClassificationOptions,
occupationCategoryOptions, occupationOptions, clientNonPersonConstitutionOptions,
clientNonPersonMainBusinessLineOptions, clientLegalFormOptions, familyMemberOptions,
new ArrayList<AddressData>(Arrays.asList(address)), isAddressEnabled, datatableTemplates);
}
private Long defaultToUsersOfficeIfNull(final Long officeId) {
@@ -125,6 +125,8 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final ClientKycWritePlatformService clientKycWritePlatformService;
private final org.apache.fineract.portfolio.mobilelookup.domain.MobileLookupLogRepository mobileLookupLogRepository;
private final org.apache.fineract.portfolio.sanctions.domain.SanctionsScreeningLogRepository sanctionsScreeningLogRepository;
@Transactional
@Override
@@ -220,6 +222,13 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
gender = this.codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.GENDER, genderId);
}
CodeValue salutation = null;
final Long salutationId = command.longValueOfParameterNamed(ClientApiConstants.salutationIdParamName);
if (salutationId != null) {
salutation = this.codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.SALUTATION,
salutationId);
}
CodeValue clientType = null;
final Long clientTypeId = command.longValueOfParameterNamed(ClientApiConstants.clientTypeIdParamName);
if (clientTypeId != null) {
@@ -248,6 +257,13 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.OCCUPATION, occupationId);
}
CodeValue maritalStatus = null;
final Long maritalStatusId = command.longValueOfParameterNamed(ClientApiConstants.maritalStatusIdParamName);
if (maritalStatusId != null) {
maritalStatus = this.codeValueRepository
.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.MARITAL_STATUS, maritalStatusId);
}
final Long savingsProductId = command.longValueOfParameterNamed(ClientApiConstants.savingsProductIdParamName);
if (savingsProductId != null) {
this.savingsProductRepository.findById(savingsProductId)
@@ -270,6 +286,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
final String accountNo = command.stringValueOfParameterNamed(ClientApiConstants.accountNoParamName);
final String cifNumber = command.stringValueOfParameterNamed(ClientApiConstants.cifNumberParamName);
final String mobileNo = command.stringValueOfParameterNamed(ClientApiConstants.mobileNoParamName);
final String alternateMobileNo = command.stringValueOfParameterNamed(ClientApiConstants.alternateMobileNoParamName);
final String emailAddress = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName);
final String firstname = command.stringValueOfParameterNamed(ClientApiConstants.firstnameParamName);
final String middlename = command.stringValueOfParameterNamed(ClientApiConstants.middlenameParamName);
@@ -305,8 +322,87 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
final Client newClient = Client.instance(currentUser, status, clientOffice, clientParentGroup, accountNo, cifNumber, firstname,
middlename, lastname, fullname, activationDate, officeJoiningDate, externalId, mobileNo, emailAddress, staff,
submittedOnDate, savingsProductId, savingsAccountId, dataOfBirth, gender, clientType, clientClassification,
occupationCategory, occupation, legalForm.getValue(), isStaff);
submittedOnDate, savingsProductId, savingsAccountId, dataOfBirth, gender, salutation, clientType,
clientClassification, occupationCategory, occupation, legalForm.getValue(), isStaff);
final java.math.BigDecimal monthlyIncome = command.bigDecimalValueOfParameterNamed(ClientApiConstants.monthlyIncomeParamName);
if (monthlyIncome != null) {
newClient.setMonthlyIncome(monthlyIncome);
}
final java.math.BigDecimal annualTurnover = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualTurnoverParamName);
if (annualTurnover != null) {
newClient.setAnnualTurnover(annualTurnover);
}
if (maritalStatus != null) {
newClient.setMaritalStatus(maritalStatus);
}
final String nationality = command.stringValueOfParameterNamed(ClientApiConstants.nationalityIdParamName);
if (nationality != null) {
newClient.setNationality(nationality);
}
final Boolean isMinor = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isMinorParamName);
if (Boolean.TRUE.equals(isMinor)) {
newClient.setIsMinor(true);
final Long guardianClientId = command.longValueOfParameterNamed(ClientApiConstants.guardianClientIdParamName);
if (guardianClientId != null) {
final Client guardianClient = this.clientRepository.findOneWithNotFoundDetection(guardianClientId);
newClient.setGuardianClient(guardianClient);
}
final String guardianRelation = command.stringValueOfParameterNamed(ClientApiConstants.guardianRelationParamName);
if (guardianRelation != null) {
newClient.setGuardianRelation(guardianRelation);
}
} else {
newClient.setIsMinor(false);
}
if (StringUtils.isNotBlank(alternateMobileNo)) {
newClient.setAlternateMobileNo(alternateMobileNo.trim());
}
final Boolean dipVerified = command.booleanObjectValueOfParameterNamed(ClientApiConstants.dipVerifiedParamName);
if (dipVerified != null) {
newClient.setDipVerified(dipVerified);
}
final Long dipLookupLogId = command.longValueOfParameterNamed(ClientApiConstants.dipLookupLogIdParamName);
if (dipLookupLogId != null) {
newClient.setDipLookupLogId(dipLookupLogId);
}
final String dipTimestamp = command.stringValueOfParameterNamed(ClientApiConstants.dipTimestampParamName);
if (dipTimestamp != null) {
newClient.setDipTimestamp(dipTimestamp);
}
final Boolean alternateDipVerified = command.booleanObjectValueOfParameterNamed(ClientApiConstants.alternateDipVerifiedParamName);
if (alternateDipVerified != null) {
newClient.setAlternateDipVerified(alternateDipVerified);
}
final Long alternateDipLookupLogId = command.longValueOfParameterNamed(ClientApiConstants.alternateDipLookupLogIdParamName);
if (alternateDipLookupLogId != null) {
newClient.setAlternateDipLookupLogId(alternateDipLookupLogId);
}
final String alternateDipTimestamp = command.stringValueOfParameterNamed(ClientApiConstants.alternateDipTimestampParamName);
if (alternateDipTimestamp != null) {
newClient.setAlternateDipTimestamp(alternateDipTimestamp);
}
final Boolean nameVerified = command.booleanObjectValueOfParameterNamed(ClientApiConstants.nameVerifiedParamName);
if (nameVerified != null) {
newClient.setNameVerified(nameVerified);
}
final Long screeningLogId = command.longValueOfParameterNamed(ClientApiConstants.screeningLogIdParamName);
if (screeningLogId != null) {
newClient.setScreeningLogId(screeningLogId);
}
// Account Number generation
this.clientRepository.saveAndFlush(newClient);
@@ -323,6 +419,28 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
this.clientRepository.saveAndFlush(newClient);
}
// Backfill client_id on DIP lookup logs
if (newClient.getDipLookupLogId() != null) {
this.mobileLookupLogRepository.findById(newClient.getDipLookupLogId()).ifPresent(log -> {
log.setClientId(newClient.getId());
this.mobileLookupLogRepository.save(log);
});
}
if (newClient.getAlternateDipLookupLogId() != null) {
this.mobileLookupLogRepository.findById(newClient.getAlternateDipLookupLogId()).ifPresent(log -> {
log.setClientId(newClient.getId());
this.mobileLookupLogRepository.save(log);
});
}
// Backfill client_id on sanctions screening log
if (newClient.getScreeningLogId() != null) {
this.sanctionsScreeningLogRepository.findById(newClient.getScreeningLogId()).ifPresent(log -> {
log.setClientId(newClient.getId());
this.sanctionsScreeningLogRepository.save(log);
});
}
boolean rollbackTransaction = false;
if (newClient.isActive()) {
validateParentGroupRulesBeforeClientActivation(newClient);
@@ -428,8 +546,16 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
ClientApiConstants.CLIENT_NON_PERSON_MAIN_BUSINESS_LINE, clientNonPersonMainBusinessLineId);
}
final String authorizedSignatory = this.fromApiJsonHelper
.extractStringNamed(ClientApiConstants.authorizedSignatoryParamName, clientNonPersonElement);
final String additionalInfo = this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.additionalInfoParamName,
clientNonPersonElement);
final LocalDate dateOfRegistration = this.fromApiJsonHelper
.extractLocalDateNamed(ClientApiConstants.dateOfRegistrationParamName, clientNonPersonElement);
final ClientNonPerson newClientNonPerson = ClientNonPerson.createNew(client, clientNonPersonConstitution,
clientNonPersonMainBusinessLine, incorpNumber, incorpValidityTill, remarks);
clientNonPersonMainBusinessLine, incorpNumber, incorpValidityTill, remarks, authorizedSignatory, additionalInfo,
dateOfRegistration);
this.clientNonPersonRepository.save(newClientNonPerson);
}
@@ -478,6 +604,12 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
clientForUpdate.setMobileNo(StringUtils.defaultIfEmpty(newValue, null));
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.alternateMobileNoParamName, clientForUpdate.getAlternateMobileNo())) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.alternateMobileNoParamName);
changes.put(ClientApiConstants.alternateMobileNoParamName, newValue);
clientForUpdate.setAlternateMobileNo(StringUtils.defaultIfEmpty(newValue, null));
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.emailAddressParamName, clientForUpdate.getEmailAddress())) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.emailAddressParamName);
changes.put(ClientApiConstants.emailAddressParamName, newValue);
@@ -518,6 +650,11 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
changes.put(ClientApiConstants.genderIdParamName, newValue);
}
if (command.isChangeInLongParameterNamed(ClientApiConstants.salutationIdParamName, clientForUpdate.salutationId())) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.salutationIdParamName);
changes.put(ClientApiConstants.salutationIdParamName, newValue);
}
if (command.isChangeInLongParameterNamed(ClientApiConstants.savingsProductIdParamName, clientForUpdate.savingsProductId())) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.savingsProductIdParamName);
changes.put(ClientApiConstants.savingsProductIdParamName, newValue);
@@ -545,6 +682,104 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
changes.put(ClientApiConstants.occupationIdParamName, newValue);
}
if (command.isChangeInLongParameterNamed(ClientApiConstants.maritalStatusIdParamName,
clientForUpdate.maritalStatusId())) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.maritalStatusIdParamName);
changes.put(ClientApiConstants.maritalStatusIdParamName, newValue);
}
if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.monthlyIncomeParamName, clientForUpdate.getMonthlyIncome())) {
final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.monthlyIncomeParamName);
changes.put(ClientApiConstants.monthlyIncomeParamName, newValue);
clientForUpdate.setMonthlyIncome(newValue);
}
if (command.isChangeInBigDecimalParameterNamed(ClientApiConstants.annualTurnoverParamName, clientForUpdate.getAnnualTurnover())) {
final java.math.BigDecimal newValue = command.bigDecimalValueOfParameterNamed(ClientApiConstants.annualTurnoverParamName);
changes.put(ClientApiConstants.annualTurnoverParamName, newValue);
clientForUpdate.setAnnualTurnover(newValue);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.nationalityIdParamName, clientForUpdate.getNationality())) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.nationalityIdParamName);
changes.put(ClientApiConstants.nationalityIdParamName, newValue);
clientForUpdate.setNationality(newValue);
}
if (command.isChangeInBooleanParameterNamed(ClientApiConstants.isMinorParamName, clientForUpdate.getIsMinor())) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.isMinorParamName);
changes.put(ClientApiConstants.isMinorParamName, newValue);
clientForUpdate.setIsMinor(newValue);
}
if (command.hasParameter(ClientApiConstants.guardianClientIdParamName)) {
final Long newGuardianId = command.longValueOfParameterNamed(ClientApiConstants.guardianClientIdParamName);
final Long currentGuardianId = clientForUpdate.getGuardianClient() != null ? clientForUpdate.getGuardianClient().getId() : null;
if (!java.util.Objects.equals(newGuardianId, currentGuardianId)) {
changes.put(ClientApiConstants.guardianClientIdParamName, newGuardianId);
if (newGuardianId != null) {
final Client guardianClient = this.clientRepository.findOneWithNotFoundDetection(newGuardianId);
clientForUpdate.setGuardianClient(guardianClient);
} else {
clientForUpdate.setGuardianClient(null);
}
}
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.guardianRelationParamName, clientForUpdate.getGuardianRelation())) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.guardianRelationParamName);
changes.put(ClientApiConstants.guardianRelationParamName, newValue);
clientForUpdate.setGuardianRelation(newValue);
}
if (command.isChangeInBooleanParameterNamed(ClientApiConstants.dipVerifiedParamName, clientForUpdate.getDipVerified())) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.dipVerifiedParamName);
changes.put(ClientApiConstants.dipVerifiedParamName, newValue);
clientForUpdate.setDipVerified(newValue);
}
if (command.isChangeInLongParameterNamed(ClientApiConstants.dipLookupLogIdParamName, clientForUpdate.getDipLookupLogId())) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.dipLookupLogIdParamName);
changes.put(ClientApiConstants.dipLookupLogIdParamName, newValue);
clientForUpdate.setDipLookupLogId(newValue);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.dipTimestampParamName, clientForUpdate.getDipTimestamp())) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.dipTimestampParamName);
changes.put(ClientApiConstants.dipTimestampParamName, newValue);
clientForUpdate.setDipTimestamp(newValue);
}
if (command.isChangeInBooleanParameterNamed(ClientApiConstants.alternateDipVerifiedParamName, clientForUpdate.getAlternateDipVerified())) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.alternateDipVerifiedParamName);
changes.put(ClientApiConstants.alternateDipVerifiedParamName, newValue);
clientForUpdate.setAlternateDipVerified(newValue);
}
if (command.isChangeInLongParameterNamed(ClientApiConstants.alternateDipLookupLogIdParamName, clientForUpdate.getAlternateDipLookupLogId())) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.alternateDipLookupLogIdParamName);
changes.put(ClientApiConstants.alternateDipLookupLogIdParamName, newValue);
clientForUpdate.setAlternateDipLookupLogId(newValue);
}
if (command.isChangeInStringParameterNamed(ClientApiConstants.alternateDipTimestampParamName, clientForUpdate.getAlternateDipTimestamp())) {
final String newValue = command.stringValueOfParameterNamed(ClientApiConstants.alternateDipTimestampParamName);
changes.put(ClientApiConstants.alternateDipTimestampParamName, newValue);
clientForUpdate.setAlternateDipTimestamp(newValue);
}
if (command.isChangeInBooleanParameterNamed(ClientApiConstants.nameVerifiedParamName, clientForUpdate.getNameVerified())) {
final Boolean newValue = command.booleanObjectValueOfParameterNamed(ClientApiConstants.nameVerifiedParamName);
changes.put(ClientApiConstants.nameVerifiedParamName, newValue);
clientForUpdate.setNameVerified(newValue);
}
if (command.isChangeInLongParameterNamed(ClientApiConstants.screeningLogIdParamName, clientForUpdate.getScreeningLogId())) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.screeningLogIdParamName);
changes.put(ClientApiConstants.screeningLogIdParamName, newValue);
clientForUpdate.setScreeningLogId(newValue);
}
if (command.isChangeInIntegerParameterNamed(ClientApiConstants.legalFormIdParamName, clientForUpdate.getLegalForm())) {
final Integer newValue = command.integerValueOfParameterNamed(ClientApiConstants.legalFormIdParamName);
if (newValue != null) {
@@ -642,6 +877,17 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
clientForUpdate.updateGender(newCodeVal);
}
if (changes.containsKey(ClientApiConstants.salutationIdParamName)) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.salutationIdParamName);
CodeValue newCodeVal = null;
if (newValue != null) {
newCodeVal = this.codeValueRepository.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.SALUTATION,
newValue);
}
clientForUpdate.updateSalutation(newCodeVal);
clientForUpdate.deriveDisplayName();
}
if (changes.containsKey(ClientApiConstants.clientTypeIdParamName)) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.clientTypeIdParamName);
CodeValue newCodeVal = null;
@@ -682,6 +928,16 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP
clientForUpdate.updateOccupation(newCodeVal);
}
if (changes.containsKey(ClientApiConstants.maritalStatusIdParamName)) {
final Long newValue = command.longValueOfParameterNamed(ClientApiConstants.maritalStatusIdParamName);
CodeValue newCodeVal = null;
if (newValue != null) {
newCodeVal = this.codeValueRepository
.findOneByCodeNameAndIdWithNotFoundDetection(ClientApiConstants.MARITAL_STATUS, newValue);
}
clientForUpdate.updateMaritalStatus(newCodeVal);
}
if (!changes.isEmpty()) {
this.clientRepository.saveAndFlush(clientForUpdate);
}
@@ -31,6 +31,7 @@ public class ClientSearchData {
private String displayName;
private ExternalId externalId;
private String accountNumber;
private String cifNumber;
private Long officeId;
private String officeName;
private String mobileNo;
@@ -27,6 +27,7 @@ import lombok.Getter;
@AllArgsConstructor
public class MobileLookupResult {
private final Long lookupLogId;
private final boolean success;
private final String message;
private final MobileLookupData data;
@@ -44,38 +45,38 @@ public class MobileLookupResult {
private final MobileLookupDetailData disconnectedDetail;
}
public static MobileLookupResult clean(String mobileNumber) {
public static MobileLookupResult clean(Long lookupLogId, String mobileNumber) {
MobileLookupData data = new MobileLookupData(mobileNumber, "CLEAN", false, false, null, null);
return new MobileLookupResult(true,
return new MobileLookupResult(lookupLogId, true,
"Number is CLEAN — not found in DIP or MNRL records",
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
}
public static MobileLookupResult suspected(String mobileNumber, MobileLookupDetailData suspectedDetail) {
public static MobileLookupResult suspected(Long lookupLogId, String mobileNumber, MobileLookupDetailData suspectedDetail) {
MobileLookupData data = new MobileLookupData(mobileNumber, "SUSPECTED", true, false, suspectedDetail, null);
return new MobileLookupResult(true,
return new MobileLookupResult(lookupLogId, true,
"Number is SUSPECTED — found in DIP records",
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
}
public static MobileLookupResult disconnected(String mobileNumber, MobileLookupDetailData disconnectedDetail) {
public static MobileLookupResult disconnected(Long lookupLogId, String mobileNumber, MobileLookupDetailData disconnectedDetail) {
MobileLookupData data = new MobileLookupData(mobileNumber, "DISCONNECTED", false, true, null, disconnectedDetail);
return new MobileLookupResult(true,
return new MobileLookupResult(lookupLogId, true,
"Number is DISCONNECTED — found in MNRL records",
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
}
public static MobileLookupResult suspectedAndDisconnected(String mobileNumber,
public static MobileLookupResult suspectedAndDisconnected(Long lookupLogId, String mobileNumber,
MobileLookupDetailData suspectedDetail, MobileLookupDetailData disconnectedDetail) {
MobileLookupData data = new MobileLookupData(mobileNumber, "SUSPECTED_AND_DISCONNECTED",
true, true, suspectedDetail, disconnectedDetail);
return new MobileLookupResult(true,
return new MobileLookupResult(lookupLogId, true,
"Number is SUSPECTED and DISCONNECTED — found in both DIP and MNRL records",
data, OffsetDateTime.now(ZoneOffset.UTC).toString());
}
public static MobileLookupResult serviceUnavailable(String mobileNumber, String message) {
public static MobileLookupResult serviceUnavailable(Long lookupLogId, String mobileNumber, String message) {
MobileLookupData data = new MobileLookupData(mobileNumber, "SERVICE_UNAVAILABLE", false, false, null, null);
return new MobileLookupResult(false, message, data, OffsetDateTime.now(ZoneOffset.UTC).toString());
return new MobileLookupResult(lookupLogId, false, message, data, OffsetDateTime.now(ZoneOffset.UTC).toString());
}
}
@@ -81,19 +81,17 @@ public class MobileLookupServiceImpl implements MobileLookupService {
LOG.info("Demo suspected number detected: {}", trimmed);
final MobileLookupDetailData suspectedDetail = MobileLookupDetailData.forSuspected(
"active", "Demo DIP suspected number", "DEMO");
final MobileLookupResult result = MobileLookupResult.suspected(trimmed, suspectedDetail);
saveLog(trimmed, "SUSPECTED", true, false,
result.getMessage(), null, source, clientId);
return result;
final Long logId = saveLog(trimmed, "SUSPECTED", true, false,
"Number is SUSPECTED — found in DIP records", null, source, clientId);
return MobileLookupResult.suspected(logId, trimmed, suspectedDetail);
}
// Demo numbers — return CLEAN without calling the external API
if (isDemoCleanNumber(trimmed)) {
LOG.info("Demo clean number detected: {}", trimmed);
final MobileLookupResult result = MobileLookupResult.clean(trimmed);
saveLog(trimmed, "CLEAN", false, false,
result.getMessage(), null, source, clientId);
return result;
final Long logId = saveLog(trimmed, "CLEAN", false, false,
"Number is CLEAN — not found in DIP or MNRL records", null, source, clientId);
return MobileLookupResult.clean(logId, trimmed);
}
final String url = baseUrl + "/api/v1/lookup/check?mobileNumber=" + trimmed;
@@ -105,19 +103,19 @@ public class MobileLookupServiceImpl implements MobileLookupService {
rawResponse = restTemplate.getForObject(url, String.class);
LOG.info("DIP/MNRL lookup response for {}: {}", trimmed, rawResponse);
final MobileLookupResult result = parseResponse(rawResponse);
final MobileLookupResult parsed = parseResponse(rawResponse);
saveLog(trimmed, result.getData().getStatus(),
result.getData().isSuspected(), result.getData().isDisconnected(),
result.getMessage(), rawResponse, source, clientId);
final Long logId = saveLog(trimmed, parsed.getData().getStatus(),
parsed.getData().isSuspected(), parsed.getData().isDisconnected(),
parsed.getMessage(), rawResponse, source, clientId);
return result;
return new MobileLookupResult(logId, parsed.isSuccess(), parsed.getMessage(),
parsed.getData(), parsed.getTimestamp());
} catch (Exception e) {
LOG.error("Failed to call DIP/MNRL lookup API for {}: {}", trimmed, e.getMessage());
saveLog(trimmed, "ERROR", false, false,
final Long logId = saveLog(trimmed, "ERROR", false, false,
e.getMessage(), rawResponse, source, clientId);
// Return CLEAN with service-unavailable message instead of throwing
return MobileLookupResult.serviceUnavailable(trimmed,
return MobileLookupResult.serviceUnavailable(logId, trimmed,
"DIP/MNRL lookup service is unavailable. Number treated as CLEAN by default.");
}
}
@@ -140,7 +138,7 @@ public class MobileLookupServiceImpl implements MobileLookupService {
return logRepository.findByLookupStatus(status);
}
private void saveLog(final String mobileNumber, final String status,
private Long saveLog(final String mobileNumber, final String status,
final boolean suspected, final boolean disconnected,
final String message, final String rawResponse,
final String source, final Long clientId) {
@@ -148,9 +146,11 @@ public class MobileLookupServiceImpl implements MobileLookupService {
final MobileLookupLog log = new MobileLookupLog(
mobileNumber, status, suspected, disconnected,
message, rawResponse, source, clientId);
logRepository.save(log);
logRepository.saveAndFlush(log);
return log.getId();
} catch (Exception e) {
LOG.error("Failed to save mobile lookup log for '{}': {}", mobileNumber, e.getMessage());
return null;
}
}
@@ -188,7 +188,7 @@ public class MobileLookupServiceImpl implements MobileLookupService {
MobileLookupResult.MobileLookupData lookupData = new MobileLookupResult.MobileLookupData(
mobileNumber, status, suspected, disconnected, suspectedDetail, disconnectedDetail);
return new MobileLookupResult(success, message, lookupData, timestamp);
return new MobileLookupResult(null, success, message, lookupData, timestamp);
}
private boolean isDemoCleanNumber(final String mobileNumber) {
@@ -25,6 +25,7 @@ import java.util.List;
*/
public final class SanctionsScreeningResult {
private final Long screeningLogId;
private final String screenedName;
private final boolean matchFound;
private final String status;
@@ -32,9 +33,10 @@ public final class SanctionsScreeningResult {
private final int matchCount;
private final List<SanctionsMatchData> matches;
private SanctionsScreeningResult(final String screenedName, final boolean matchFound,
private SanctionsScreeningResult(final Long screeningLogId, final String screenedName, final boolean matchFound,
final String status, final String message, final int matchCount,
final List<SanctionsMatchData> matches) {
this.screeningLogId = screeningLogId;
this.screenedName = screenedName;
this.matchFound = matchFound;
this.status = status;
@@ -43,20 +45,21 @@ public final class SanctionsScreeningResult {
this.matches = matches;
}
public static SanctionsScreeningResult matched(final String screenedName,
public static SanctionsScreeningResult matched(final Long screeningLogId, final String screenedName,
final List<SanctionsMatchData> matches) {
return new SanctionsScreeningResult(screenedName, true, "FLAGGED",
return new SanctionsScreeningResult(screeningLogId, screenedName, true, "FLAGGED",
"Potential match found in UN Security Council consolidated sanctions list. "
+ "Manual review required before proceeding.",
matches.size(), matches);
}
public static SanctionsScreeningResult clear(final String screenedName) {
return new SanctionsScreeningResult(screenedName, false, "CLEAR",
public static SanctionsScreeningResult clear(final Long screeningLogId, final String screenedName) {
return new SanctionsScreeningResult(screeningLogId, screenedName, false, "CLEAR",
"No match found in UN Security Council consolidated sanctions list.",
0, List.of());
}
public Long getScreeningLogId() { return screeningLogId; }
public String getScreenedName() { return screenedName; }
public boolean isMatchFound() { return matchFound; }
public String getStatus() { return status; }
@@ -53,8 +53,8 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
public SanctionsScreeningResult screenName(final String name, final String source,
final Long clientId, final String ipAddress) {
if (name == null || name.isBlank()) {
saveLog(name, "CLEAR", 0, null, null, source, clientId, ipAddress);
return SanctionsScreeningResult.clear(name);
final Long logId = saveLog(name, "CLEAR", 0, null, null, source, clientId, ipAddress);
return SanctionsScreeningResult.clear(logId, name);
}
final String trimmedName = name.trim();
@@ -64,8 +64,8 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
if (matches.isEmpty()) {
LOG.info("No sanctions match found for: {}", trimmedName);
saveLog(trimmedName, "CLEAR", 0, null, null, source, clientId, ipAddress);
return SanctionsScreeningResult.clear(trimmedName);
final Long logId = saveLog(trimmedName, "CLEAR", 0, null, null, source, clientId, ipAddress);
return SanctionsScreeningResult.clear(logId, trimmedName);
}
LOG.warn("Sanctions match found for '{}': {} potential match(es)", trimmedName, matches.size());
@@ -82,9 +82,9 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
.map(SanctionsEntry::getFullName)
.collect(Collectors.joining(", "));
saveLog(trimmedName, "FLAGGED", matches.size(), matchedEntryIds, matchedNames, source, clientId, ipAddress);
final Long logId = saveLog(trimmedName, "FLAGGED", matches.size(), matchedEntryIds, matchedNames, source, clientId, ipAddress);
return SanctionsScreeningResult.matched(trimmedName, matchDataList);
return SanctionsScreeningResult.matched(logId, trimmedName, matchDataList);
}
@Override
@@ -111,16 +111,18 @@ public class SanctionsScreeningServiceImpl implements SanctionsScreeningService
return screeningLogRepository.findByScreenedNameContaining(name);
}
private void saveLog(final String screenedName, final String result, final int matchCount,
private Long saveLog(final String screenedName, final String result, final int matchCount,
final String matchedEntryIds, final String matchedNames, final String source,
final Long clientId, final String ipAddress) {
try {
final SanctionsScreeningLog log = new SanctionsScreeningLog(
screenedName, result, matchCount, matchedEntryIds, matchedNames,
source, clientId, ipAddress);
screeningLogRepository.save(log);
screeningLogRepository.saveAndFlush(log);
return log.getId();
} catch (Exception e) {
LOG.error("Failed to save sanctions screening log for '{}': {}", screenedName, e.getMessage());
return null;
}
}
@@ -652,6 +652,8 @@ fineract.kyc.lincpay.path.aadhaar-generate-otp=${FINERACT_KYC_LINCPAY_PATH_AADHA
fineract.kyc.lincpay.path.aadhaar-verify-otp=${FINERACT_KYC_LINCPAY_PATH_AADHAAR_VERIFY_OTP:/aadhaar/verify-otp}
fineract.kyc.lincpay.path.pan-validate=${FINERACT_KYC_LINCPAY_PATH_PAN_VALIDATE:/validate-pan}
fineract.kyc.lincpay.path.gstin-validate=${FINERACT_KYC_LINCPAY_PATH_GSTIN_VALIDATE:/validate-gstn}
fineract.kyc.lincpay.path.cin-validate=${FINERACT_KYC_LINCPAY_PATH_CIN_VALIDATE:/validate-company}
fineract.kyc.lincpay.path.din-validate=${FINERACT_KYC_LINCPAY_PATH_DIN_VALIDATE:/validate-director}
# ── OpenProject Integration ──────────────────────────────────────────────────
# Set FINERACT_OPENPROJECT_API_TOKEN as env var in production.
@@ -247,4 +247,25 @@
<include file="parts/0226_add_cfcfrms_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0227_add_client_cif_number.xml" relativeToChangelogFile="true"/>
<include file="parts/0228_add_occupation_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0229_add_salutation_field.xml" relativeToChangelogFile="true"/>
<include file="parts/0230_add_client_form60.xml" relativeToChangelogFile="true"/>
<include file="parts/0231_add_menu_permissions.xml" relativeToChangelogFile="true"/>
<include file="parts/0232_add_pan_name_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0233_auto_verify_kyc_status.xml" relativeToChangelogFile="true"/>
<include file="parts/0234_add_monthly_income.xml" relativeToChangelogFile="true"/>
<include file="parts/0235_add_form60_purpose_code.xml" relativeToChangelogFile="true"/>
<include file="parts/0236_add_marital_status.xml" relativeToChangelogFile="true"/>
<include file="parts/0237_extend_family_members.xml" relativeToChangelogFile="true"/>
<include file="parts/0238_add_annual_turnover.xml" relativeToChangelogFile="true"/>
<include file="parts/0239_add_minor_guardian_nationality.xml" relativeToChangelogFile="true"/>
<include file="parts/0240_add_entity_classification_tables.xml" relativeToChangelogFile="true"/>
<include file="parts/0241_add_client_nominee_table.xml" relativeToChangelogFile="true"/>
<include file="parts/0242_add_marriage_authority.xml" relativeToChangelogFile="true"/>
<include file="parts/0243_add_nominee_identifier.xml" relativeToChangelogFile="true"/>
<include file="parts/0244_add_alternate_mobile.xml" relativeToChangelogFile="true"/>
<include file="parts/0245_add_address_client_fk_and_types.xml" relativeToChangelogFile="true"/>
<include file="parts/0246_add_kyc_extended_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0247_add_huf_client_classification.xml" relativeToChangelogFile="true"/>
<include file="parts/0248_add_cin_din_verification_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0249_add_non_person_extended_fields.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -39,7 +39,7 @@
<!-- ═══════════════════════════════════════════════════════════════════════
MySQL – m_cfcfrms_complaint_ticket
═══════════════════════════════════════════════════════════════════ -->
<changeSet author="fineract" id="1" context="mysql">
<changeSet author="fineract" id="0226-1" context="mysql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cfcfrms_complaint_ticket"/></not>
</preConditions>
@@ -110,7 +110,7 @@
</changeSet>
<!-- PostgreSQL – m_cfcfrms_complaint_ticket -->
<changeSet author="fineract" id="1" context="postgresql">
<changeSet author="fineract" id="0226-2" context="postgresql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cfcfrms_complaint_ticket"/></not>
</preConditions>
@@ -183,7 +183,7 @@
<!-- ═══════════════════════════════════════════════════════════════════════
MySQL – m_cfcfrms_complaint_audit_log
═══════════════════════════════════════════════════════════════════ -->
<changeSet author="fineract" id="2" context="mysql">
<changeSet author="fineract" id="0226-3" context="mysql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cfcfrms_complaint_audit_log"/></not>
</preConditions>
@@ -220,7 +220,7 @@
</changeSet>
<!-- PostgreSQL – m_cfcfrms_complaint_audit_log -->
<changeSet author="fineract" id="2" context="postgresql">
<changeSet author="fineract" id="0226-4" context="postgresql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cfcfrms_complaint_audit_log"/></not>
</preConditions>
@@ -259,7 +259,7 @@
<!-- ═══════════════════════════════════════════════════════════════════════
MySQL – m_cfcfrms_simulator_log
═══════════════════════════════════════════════════════════════════ -->
<changeSet author="fineract" id="3" context="mysql">
<changeSet author="fineract" id="0226-5" context="mysql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cfcfrms_simulator_log"/></not>
</preConditions>
@@ -299,7 +299,7 @@
</changeSet>
<!-- PostgreSQL – m_cfcfrms_simulator_log -->
<changeSet author="fineract" id="3" context="postgresql">
<changeSet author="fineract" id="0226-6" context="postgresql">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_cfcfrms_simulator_log"/></not>
</preConditions>
@@ -339,7 +339,7 @@
</changeSet>
<!-- Indexes -->
<changeSet author="fineract" id="4">
<changeSet author="fineract" id="0226-7">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_cfcfrms_ticket_id"/></not>
</preConditions>
@@ -348,7 +348,7 @@
</createIndex>
</changeSet>
<changeSet author="fineract" id="5">
<changeSet author="fineract" id="0226-8">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_cfcfrms_ticket_status"/></not>
</preConditions>
@@ -357,7 +357,7 @@
</createIndex>
</changeSet>
<changeSet author="fineract" id="6">
<changeSet author="fineract" id="0226-9">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_cfcfrms_fraud_account"/></not>
</preConditions>
@@ -366,7 +366,7 @@
</createIndex>
</changeSet>
<changeSet author="fineract" id="7">
<changeSet author="fineract" id="0226-10">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_cfcfrms_audit_ticket_id"/></not>
</preConditions>
@@ -375,7 +375,7 @@
</createIndex>
</changeSet>
<changeSet author="fineract" id="8">
<changeSet author="fineract" id="0226-11">
<preConditions onFail="MARK_RAN">
<not><indexExists indexName="idx_cfcfrms_sim_ref"/></not>
</preConditions>
@@ -387,7 +387,7 @@
<!-- ═══════════════════════════════════════════════════════════════════════
Permissions
═══════════════════════════════════════════════════════════════════ -->
<changeSet author="fineract" id="9">
<changeSet author="fineract" id="0226-12">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_permission WHERE code = 'READ_CFCFRMS'
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- ==================== Add Salutation code ==================== -->
<changeSet author="fineract" id="0229-1">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code WHERE code_name = 'Salutation'</sqlCheck>
</preConditions>
<insert tableName="m_code">
<column name="code_name" value="Salutation"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
</changeSet>
<changeSet author="fineract" id="0229-2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value WHERE code_id = (SELECT id FROM m_code WHERE code_name = 'Salutation')</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT id, val, pos, true, false FROM m_code
CROSS JOIN (VALUES
('Mr.', 1),
('Mrs.', 2),
('Ms.', 3),
('Miss', 4),
('Dr.', 5),
('Shri', 6),
('Smt.', 7)
) AS t(val, pos)
WHERE code_name = 'Salutation';
</sql>
</changeSet>
<!-- ==================== Add salutation_cv_id column to m_client ==================== -->
<changeSet author="fineract" id="0229-3">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client" columnName="salutation_cv_id"/></not>
</preConditions>
<addColumn tableName="m_client">
<column name="salutation_cv_id" type="BIGINT">
<constraints nullable="true" />
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0229-4">
<preConditions onFail="MARK_RAN">
<not><foreignKeyConstraintExists foreignKeyName="FK_m_client_salutation_cv_id"/></not>
</preConditions>
<addForeignKeyConstraint baseTableName="m_client" baseColumnNames="salutation_cv_id"
referencedTableName="m_code_value" referencedColumnNames="id"
constraintName="FK_m_client_salutation_cv_id" />
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- ==================== Create m_client_form60 table ==================== -->
<changeSet author="fineract" id="0230-1">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_form60"/></not>
</preConditions>
<createTable tableName="m_client_form60">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="full_name" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="date_of_birth" type="DATE"/>
<column name="father_name" type="VARCHAR(200)"/>
<column name="address" type="VARCHAR(500)"/>
<column name="transaction_type" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="transaction_amount" type="DECIMAL(19,6)"/>
<column name="estimated_total_income" type="DECIMAL(19,6)"/>
<column name="reason_for_no_pan" type="VARCHAR(200)"/>
<column name="declaration" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="submitted_on" type="DATE"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_form60" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_form60_client_id"/>
</changeSet>
<!-- ==================== Add permissions ==================== -->
<changeSet author="fineract" id="0230-2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'CREATE_CLIENTFORM60'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_CLIENTFORM60"/>
<column name="entity_name" value="CLIENTFORM60"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_CLIENTFORM60"/>
<column name="entity_name" value="CLIENTFORM60"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,304 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- ==================== Heading Menu Permissions (Top Nav Bar) ==================== -->
<changeSet author="fineract" id="0231-1">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'MENU_INSTITUTION'</sqlCheck>
</preConditions>
<!-- Row 1 — 11 items -->
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_INSTITUTION"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="INSTITUTION"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_TASKS"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="TASKS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_ACCOUNTING"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="ACCOUNTING"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_REPORTS"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="REPORTS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_ADMIN"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="ADMIN"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_SUB_LEDGER"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="SUB_LEDGER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_GENERAL_LEDGER"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="GENERAL_LEDGER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_CASHIER"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="CASHIER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_OFFICER"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="OFFICER"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_DEPOSITS"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="DEPOSITS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_CLEARING"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="CLEARING"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<!-- Row 2 — 11 items -->
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_HRM"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="HRM"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_SHARE"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="SHARE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_OTHERS"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="OTHERS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_NPA"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="NPA"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_ADVANCES"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="ADVANCES"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_INSURANCE"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="INSURANCE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_RBI"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="RBI"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_DIGITAL"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="DIGITAL"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_CONFIG_WIZARD"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="CONFIG_WIZARD"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_CKYC_MGMT"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="CKYC_MGMT"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="heading_menu"/>
<column name="code" value="MENU_LETTER_MGMT"/>
<column name="entity_name" value="MENU"/>
<column name="action_name" value="LETTER_MGMT"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- ==================== Left Panel Menu Permissions (Sidebar) ==================== -->
<changeSet author="fineract" id="0231-2">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'SIDEBAR_DASHBOARD'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_DASHBOARD"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="DASHBOARD"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_NAVIGATION"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="NAVIGATION"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_CKYC"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="CKYC"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_CALENDAR"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="CALENDAR"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_CHECKER_INBOX"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="CHECKER_INBOX"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_COLLECTION_SHEET"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="COLLECTION_SHEET"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_REMITTANCES"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="REMITTANCES"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_ANALYTICS"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="ANALYTICS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_CFCFRMS"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="CFCFRMS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_RBI_INTEGRATION"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="RBI_INTEGRATION"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_RBI_CIRCULARS"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="RBI_CIRCULARS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_NOTIFICATIONS"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="NOTIFICATIONS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_FREQUENT_POSTINGS"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="FREQUENT_POSTINGS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_JOURNAL_ENTRY"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="JOURNAL_ENTRY"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="sidebar_menu"/>
<column name="code" value="SIDEBAR_CHART_OF_ACCOUNTS"/>
<column name="entity_name" value="SIDEBAR"/>
<column name="action_name" value="CHART_OF_ACCOUNTS"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Add first_name and last_name from PAN verification response -->
<changeSet author="fineract" id="0232-1">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_client_kyc" columnName="pan_first_name"/></not>
</preConditions>
<addColumn tableName="m_client_kyc">
<column name="pan_first_name" type="VARCHAR(200)"/>
<column name="pan_last_name" type="VARCHAR(200)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Auto-promote KYC status to VERIFIED (300) for Individual clients
that already have both PAN and Aadhaar verified -->
<changeSet author="fineract" id="0233-1" context="default">
<sql>
UPDATE m_client_kyc
SET kyc_status = 300,
verified_on = CURRENT_DATE
WHERE kyc_type = 1
AND pan_verified = true
AND aadhaar_verified = true
AND kyc_status != 300
AND kyc_status != 400;
</sql>
</changeSet>
<!-- Auto-promote KYC status to VERIFIED (300) for Non-Individual clients
that already have both PAN and GSTIN verified -->
<changeSet author="fineract" id="0233-2" context="default">
<sql>
UPDATE m_client_kyc
SET kyc_status = 300,
verified_on = CURRENT_DATE
WHERE kyc_type = 2
AND pan_verified = true
AND gstin_verified = true
AND kyc_status != 300
AND kyc_status != 400;
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0234-1" context="default">
<addColumn tableName="m_client">
<column name="monthly_income" type="DECIMAL(19,2)" defaultValue="null">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Create the FORM_60_PURPOSE code -->
<changeSet author="fineract" id="0235-1" context="default">
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'FORM_60_PURPOSE', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'FORM_60_PURPOSE');
</sql>
</changeSet>
<!-- Insert all purpose values -->
<changeSet author="fineract" id="0235-2" context="default">
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT id, v.code_value, v.order_position, true, false
FROM m_code,
(VALUES
('Opening Bank Account', 1),
('Cash Deposit in Bank', 2),
('Cash Withdrawal from Bank', 3),
('Fixed Deposit / Term Deposit', 4),
('Recurring Deposit', 5),
('Purchase of Bank Draft / Pay Order / Bankers Cheque', 6),
('Credit Card Application', 7),
('Debit Card Application', 8),
('Loan Application', 9),
('Personal Loan', 10),
('Home Loan', 11),
('Vehicle Loan', 12),
('Education Loan', 13),
('Business Loan', 14),
('Agricultural Loan', 15),
('Gold Loan - New Loan', 16),
('Gold Loan - Renewal', 17),
('Gold Loan - Top Up', 18),
('Gold Loan - Closure / Settlement', 19),
('Gold Loan - Interest Payment', 20),
('Gold Loan - Partial Payment', 21),
('Gold Loan - Pledge of Gold Jewellery', 22),
('Gold Loan - Release of Gold Jewellery', 23),
('Loan Against Gold Coins', 24),
('Loan Against Gold Ornaments', 25),
('Agricultural Gold Loan', 26),
('Purchase of Motor Vehicle', 27),
('Purchase of Immovable Property', 28),
('Sale of Immovable Property', 29),
('Purchase of Securities / Shares', 30),
('Purchase of Mutual Funds', 31),
('Opening Demat Account', 32),
('Life Insurance Premium Payment', 33),
('Foreign Travel Payment', 34),
('Purchase of Foreign Currency / Forex', 35),
('Hotel or Restaurant Payment above prescribed limit', 36),
('Purchase of Goods or Services above prescribed limit', 37),
('Purchase of Gold Jewellery', 38),
('Purchase of Gold Coins', 39),
('Purchase of Gold Bullion', 40),
('Sale of Gold Jewellery', 41),
('Sale of Gold Coins', 42),
('Sale of Gold Bullion', 43),
('Gold Investment Purchase', 44),
('Sovereign Gold Bond Purchase', 45),
('Gold ETF Investment', 46),
('Digital Gold Purchase', 47),
('Gold Savings Scheme', 48),
('Gold Deposit Scheme', 49),
('Investment in Bonds / Debentures', 50),
('Opening Post Office Savings Account', 51),
('Opening Trading Account', 52),
('Large Cash Transaction', 53),
('High Value Financial Transaction', 54),
('Other Financial Transaction', 55)
) AS v(code_value, order_position)
WHERE m_code.code_name = 'FORM_60_PURPOSE';
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="0236_01_insert_marital_status_code" author="fineract">
<sql>
INSERT INTO m_code (code_name, is_system_defined)
SELECT 'MaritalStatus', false
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'MaritalStatus');
</sql>
</changeSet>
<changeSet id="0236_02_insert_marital_status_values" author="fineract">
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, v.code_value, v.order_position, true, false
FROM m_code c
CROSS JOIN (
VALUES
('Single', 1),
('Married', 2),
('Unmarried', 3),
('Divorced', 4),
('Widowed', 5),
('Separated', 6),
('Annulled', 7),
('Live-in Relationship', 8),
('Other', 9),
('Not Disclosed', 10)
) AS v(code_value, order_position)
WHERE c.code_name = 'MaritalStatus'
AND NOT EXISTS (
SELECT 1 FROM m_code_value cv
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
);
</sql>
</changeSet>
<changeSet id="0236_03_add_marital_status_to_client" author="fineract">
<addColumn tableName="m_client">
<column name="marital_status_cv_id" type="BIGINT"/>
</addColumn>
<addForeignKeyConstraint baseTableName="m_client" baseColumnNames="marital_status_cv_id"
constraintName="fk_client_marital_status_cv"
referencedTableName="m_code_value" referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Add new columns to m_family_members for CBS requirements -->
<changeSet author="fineract" id="0237-1" context="default">
<addColumn tableName="m_family_members">
<column name="aadhaar_number" type="VARCHAR(12)">
<constraints nullable="true"/>
</column>
<column name="pan_number" type="VARCHAR(10)">
<constraints nullable="true"/>
</column>
<column name="nationality" type="VARCHAR(50)">
<constraints nullable="true"/>
</column>
<column name="is_alive" type="BOOLEAN" defaultValueBoolean="true">
<constraints nullable="true"/>
</column>
<column name="linked_client_id" type="BIGINT">
<constraints nullable="true"/>
</column>
<column name="marriage_date" type="DATE">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<!-- FK for linked_client_id -->
<changeSet author="fineract" id="0237-2" context="default">
<addForeignKeyConstraint
baseTableName="m_family_members"
baseColumnNames="linked_client_id"
referencedTableName="m_client"
referencedColumnNames="id"
constraintName="fk_family_member_linked_client"
onDelete="SET NULL"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0238-1" context="default">
<addColumn tableName="m_client">
<column name="annual_turnover" type="DECIMAL(19,2)" defaultValue="null"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0239-1" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="nationality"/></not></preConditions>
<addColumn tableName="m_client">
<column name="nationality" type="VARCHAR(100)" defaultValue="null"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-2" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="is_minor"/></not></preConditions>
<addColumn tableName="m_client">
<column name="is_minor" type="BOOLEAN" defaultValueBoolean="false"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-3" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="guardian_client_id"/></not></preConditions>
<addColumn tableName="m_client">
<column name="guardian_client_id" type="BIGINT" defaultValue="null"/>
</addColumn>
<addForeignKeyConstraint baseTableName="m_client" baseColumnNames="guardian_client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="fk_client_guardian_client"/>
</changeSet>
<changeSet author="fineract" id="0239-4" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="guardian_relation"/></not></preConditions>
<addColumn tableName="m_client">
<column name="guardian_relation" type="VARCHAR(50)" defaultValue="null"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-5" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="dip_verified"/></not></preConditions>
<addColumn tableName="m_client">
<column name="dip_verified" type="BOOLEAN" defaultValueBoolean="false"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-6" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="dip_lookup_log_id"/></not></preConditions>
<addColumn tableName="m_client">
<column name="dip_lookup_log_id" type="BIGINT" defaultValue="null"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-7" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="dip_timestamp"/></not></preConditions>
<addColumn tableName="m_client">
<column name="dip_timestamp" type="VARCHAR(50)" defaultValue="null"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-8" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="name_verified"/></not></preConditions>
<addColumn tableName="m_client">
<column name="name_verified" type="BOOLEAN" defaultValueBoolean="false"/>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0239-9" context="default">
<preConditions onFail="MARK_RAN"><not><columnExists tableName="m_client" columnName="screening_log_id"/></not></preConditions>
<addColumn tableName="m_client">
<column name="screening_log_id" type="BIGINT" defaultValue="null"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,300 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- ==================== 1. Startup India (DPIIT) ==================== -->
<changeSet author="fineract" id="0240-1">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_startup_india"/></not>
</preConditions>
<createTable tableName="m_client_startup_india">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="dpiit_recognition_number" type="VARCHAR(50)"/>
<column name="recognition_date" type="DATE"/>
<column name="certificate_expiry_date" type="DATE"/>
<column name="entity_stage" type="VARCHAR(30)"/>
<column name="sector_as_dpiit" type="VARCHAR(50)"/>
<column name="nic_industry_code" type="VARCHAR(20)"/>
<column name="tax_exemption_80iac" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="sidbi_fund" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="self_certification" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="fast_track_patent" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="govt_tender_exemption" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="credit_guarantee_cgss" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="psl_loan_limit" type="DECIMAL(19,2)"/>
<column name="psl_sub_category" type="VARCHAR(50)"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_startup_india" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_startup_india_client_id"/>
</changeSet>
<!-- ==================== 2. Mudra Borrower (PMMY) ==================== -->
<changeSet author="fineract" id="0240-2">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_mudra_borrower"/></not>
</preConditions>
<createTable tableName="m_client_mudra_borrower">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="mudra_category" type="VARCHAR(20)"/>
<column name="mudra_loan_account_no" type="VARCHAR(50)"/>
<column name="lending_institution_name" type="VARCHAR(100)"/>
<column name="sanctioned_amount" type="DECIMAL(19,2)"/>
<column name="disbursement_date" type="DATE"/>
<column name="loan_purpose" type="VARCHAR(50)"/>
<column name="mudra_card_number" type="VARCHAR(20)"/>
<column name="current_outstanding" type="DECIMAL(19,2)"/>
<column name="repayment_status" type="VARCHAR(30)"/>
<column name="npa_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_mudra_borrower" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_mudra_borrower_client_id"/>
</changeSet>
<!-- ==================== 3. Export Oriented Unit (EOU) ==================== -->
<changeSet author="fineract" id="0240-3">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_eou"/></not>
</preConditions>
<createTable tableName="m_client_eou">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="eou_registration_number" type="VARCHAR(50)"/>
<column name="registered_dc_zone" type="VARCHAR(50)"/>
<column name="bond_lut_number" type="VARCHAR(50)"/>
<column name="validity_from" type="DATE"/>
<column name="validity_to" type="DATE"/>
<column name="gstin_eou" type="VARCHAR(15)"/>
<column name="annual_export_obligation" type="DECIMAL(19,2)"/>
<column name="nfe_target" type="DECIMAL(19,2)"/>
<column name="softex_filing_status" type="VARCHAR(20)"/>
<column name="brc_submission_status" type="VARCHAR(20)"/>
<column name="ad_category_bank_code" type="VARCHAR(20)"/>
<column name="fema_declaration_date" type="DATE"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_eou" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_eou_client_id"/>
</changeSet>
<!-- ==================== 4. Import/Export (IEC / DGFT) ==================== -->
<changeSet author="fineract" id="0240-4">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_import_export"/></not>
</preConditions>
<createTable tableName="m_client_import_export">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="iec_code" type="VARCHAR(10)"/>
<column name="iec_issue_date" type="DATE"/>
<column name="iec_status" type="VARCHAR(20)"/>
<column name="trade_type" type="VARCHAR(30)"/>
<column name="primary_hs_code" type="VARCHAR(20)"/>
<column name="key_trading_countries" type="VARCHAR(200)"/>
<column name="ad_category_bank_code" type="VARCHAR(20)"/>
<column name="ecgc_policy_number" type="VARCHAR(50)"/>
<column name="ecgc_policy_expiry" type="DATE"/>
<column name="eefc_account_required" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="pcfc_facility_required" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="fema_declaration_obtained" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_import_export" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_import_export_client_id"/>
</changeSet>
<!-- ==================== 5. Priority Sector Lending (PSL) ==================== -->
<changeSet author="fineract" id="0240-5">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_priority_sector"/></not>
</preConditions>
<createTable tableName="m_client_priority_sector">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="psl_category" type="VARCHAR(30)"/>
<column name="psl_sub_category" type="VARCHAR(50)"/>
<column name="psl_loan_purpose_code" type="VARCHAR(10)"/>
<column name="psl_eligibility_basis" type="VARCHAR(100)"/>
<column name="fidd_reporting_code" type="VARCHAR(20)"/>
<column name="psl_sanctioned_limit" type="DECIMAL(19,2)"/>
<column name="msme_classification" type="VARCHAR(50)"/>
<column name="udyam_registration_number" type="VARCHAR(30)"/>
<column name="udyam_registration_date" type="DATE"/>
<column name="annual_turnover" type="DECIMAL(19,2)"/>
<column name="investment_plant_machinery" type="DECIMAL(19,2)"/>
<column name="weaker_section_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="sc_st_borrower_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="women_entrepreneur_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="minority_community_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_priority_sector" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_priority_sector_client_id"/>
</changeSet>
<!-- ==================== 6. Non-Priority Sector (NPS) ==================== -->
<changeSet author="fineract" id="0240-6">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="m_client_non_priority_sector"/></not>
</preConditions>
<createTable tableName="m_client_non_priority_sector">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="industry_sector" type="VARCHAR(50)"/>
<column name="nic_code" type="VARCHAR(10)"/>
<column name="industry_description" type="VARCHAR(200)"/>
<column name="annual_revenue_band" type="VARCHAR(30)"/>
<column name="risk_category" type="VARCHAR(20)"/>
<column name="customer_risk_score" type="INTEGER"/>
<column name="aml_risk_classification" type="VARCHAR(50)"/>
<column name="pep_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="aml_watchlist_match" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="adverse_media_flag" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_non_priority_sector" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="FK_m_client_non_priority_sector_client_id"/>
</changeSet>
<!-- ==================== 7. Permissions ==================== -->
<changeSet author="fineract" id="0240-7">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'READ_ENTITYCLASSIFICATION'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_ENTITYCLASSIFICATION"/>
<column name="entity_name" value="ENTITYCLASSIFICATION"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_ENTITYCLASSIFICATION"/>
<column name="entity_name" value="ENTITYCLASSIFICATION"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_ENTITYCLASSIFICATION"/>
<column name="entity_name" value="ENTITYCLASSIFICATION"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Table 7: m_client_msme -->
<changeSet author="fineract" id="0240-8" context="default">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_client_msme"/></not></preConditions>
<createTable tableName="m_client_msme">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="enabled" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="udyam_registration_number" type="VARCHAR(30)"/>
<column name="registration_date" type="DATE"/>
<column name="registration_status" type="VARCHAR(20)"/>
<column name="msme_classification" type="VARCHAR(20)"/>
<column name="enterprise_type" type="VARCHAR(30)"/>
<column name="nic_activity_code" type="VARCHAR(20)"/>
<column name="annual_turnover" type="DECIMAL(19,2)"/>
<column name="investment_plant_machinery" type="DECIMAL(19,2)"/>
<column name="financial_year_assessment" type="VARCHAR(10)"/>
<column name="social_category_owner" type="VARCHAR(30)"/>
<column name="gender_primary_owner" type="VARCHAR(20)"/>
<column name="no_of_employees" type="INTEGER"/>
<column name="women_led_enterprise" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="divyang_pwd_owned" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_msme" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="fk_msme_client_id"/>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0241-1" context="default">
<preConditions onFail="MARK_RAN"><not><tableExists tableName="m_client_nominee"/></not></preConditions>
<createTable tableName="m_client_nominee">
<column name="id" type="BIGINT" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="nominee_client_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="relationship_cv_id" type="BIGINT"/>
<column name="percentage_share" type="DECIMAL(5,2)"/>
<column name="is_active" type="BOOLEAN" defaultValueBoolean="true"/>
<column name="nominee_is_minor" type="BOOLEAN" defaultValueBoolean="false"/>
<column name="guardian_client_id" type="BIGINT"/>
<column name="guardian_relationship_cv_id" type="BIGINT"/>
<column name="effective_date" type="DATE"/>
<column name="remarks" type="VARCHAR(500)"/>
<column name="created_by" type="BIGINT"/>
<column name="created_on_utc" type="TIMESTAMP"/>
<column name="last_modified_by" type="BIGINT"/>
<column name="last_modified_on_utc" type="TIMESTAMP"/>
</createTable>
<addForeignKeyConstraint baseTableName="m_client_nominee" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="fk_nominee_client_id"/>
<addForeignKeyConstraint baseTableName="m_client_nominee" baseColumnNames="nominee_client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="fk_nominee_nominee_client_id"/>
<addForeignKeyConstraint baseTableName="m_client_nominee" baseColumnNames="guardian_client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="fk_nominee_guardian_client_id"/>
<addForeignKeyConstraint baseTableName="m_client_nominee" baseColumnNames="relationship_cv_id"
referencedTableName="m_code_value" referencedColumnNames="id"
constraintName="fk_nominee_relationship_cv"/>
<addForeignKeyConstraint baseTableName="m_client_nominee" baseColumnNames="guardian_relationship_cv_id"
referencedTableName="m_code_value" referencedColumnNames="id"
constraintName="fk_nominee_guardian_rel_cv"/>
</changeSet>
<!-- Add RELATIONSHIP code if not exists -->
<changeSet author="fineract" id="0241-2" context="default">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code WHERE code_name = 'RELATIONSHIP'</sqlCheck>
</preConditions>
<insert tableName="m_code">
<column name="code_name" value="RELATIONSHIP"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
</changeSet>
<!-- Add relationship code values -->
<changeSet author="fineract" id="0241-3" context="default">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id WHERE c.code_name = 'RELATIONSHIP'</sqlCheck>
</preConditions>
<sql>
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
SELECT c.id, v.val, v.pos, true, false
FROM m_code c
CROSS JOIN (VALUES
('FATHER', 1), ('MOTHER', 2), ('SPOUSE', 3), ('SON', 4), ('DAUGHTER', 5),
('BROTHER', 6), ('SISTER', 7), ('GRANDFATHER', 8), ('GRANDMOTHER', 9),
('UNCLE', 10), ('AUNT', 11), ('NEPHEW', 12), ('NIECE', 13),
('FATHER_IN_LAW', 14), ('MOTHER_IN_LAW', 15), ('FRIEND', 16), ('OTHER', 17)
) AS v(val, pos)
WHERE c.code_name = 'RELATIONSHIP';
</sql>
</changeSet>
<!-- Permissions -->
<changeSet author="fineract" id="0241-4" context="default">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(*) FROM m_permission WHERE code = 'READ_CLIENTNOMINEE'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_CLIENTNOMINEE"/>
<column name="entity_name" value="CLIENTNOMINEE"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_CLIENTNOMINEE"/>
<column name="entity_name" value="CLIENTNOMINEE"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_CLIENTNOMINEE"/>
<column name="entity_name" value="CLIENTNOMINEE"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_CLIENTNOMINEE"/>
<column name="entity_name" value="CLIENTNOMINEE"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Add marriage_authority column to m_family_members -->
<changeSet author="fineract" id="0242-1" context="default">
<addColumn tableName="m_family_members">
<column name="marriage_authority" type="VARCHAR(50)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<!-- Add permissions for family member documents (using existing m_document infrastructure) -->
<changeSet author="fineract" id="0242-2" context="default">
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_FAMILYMEMBERDOCUMENT"/>
<column name="entity_name" value="FAMILYMEMBERDOCUMENT"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_FAMILYMEMBERDOCUMENT"/>
<column name="entity_name" value="FAMILYMEMBERDOCUMENT"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_FAMILYMEMBERDOCUMENT"/>
<column name="entity_name" value="FAMILYMEMBERDOCUMENT"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Add nominee_identifier, account_no, account_type to m_client_nominee -->
<changeSet author="fineract" id="0243-1" context="default">
<addColumn tableName="m_client_nominee">
<column name="nominee_identifier" type="VARCHAR(50)">
<constraints nullable="true" unique="true" uniqueConstraintName="uq_nominee_identifier"/>
</column>
<column name="account_no" type="VARCHAR(50)">
<constraints nullable="true"/>
</column>
<column name="account_type" type="VARCHAR(10)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0243-2" context="default">
<createIndex tableName="m_client_nominee" indexName="idx_nominee_account_no_type">
<column name="account_no"/>
<column name="account_type"/>
</createIndex>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Add alternate mobile number and its DIP verification fields to m_client -->
<changeSet author="fineract" id="0244-1" context="default">
<addColumn tableName="m_client">
<column name="alternate_mobile_no" type="VARCHAR(50)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0244-2" context="default">
<addColumn tableName="m_client">
<column name="alternate_dip_verified" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0244-3" context="default">
<addColumn tableName="m_client">
<column name="alternate_dip_lookup_log_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0244-4" context="default">
<addColumn tableName="m_client">
<column name="alternate_dip_timestamp" type="VARCHAR(50)">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<!-- Add client_id FK column to m_address -->
<changeSet author="fineract" id="0245-1" context="default">
<addColumn tableName="m_address">
<column name="client_id" type="BIGINT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0245-2" context="default">
<addForeignKeyConstraint baseTableName="m_address" baseColumnNames="client_id"
referencedTableName="m_client" referencedColumnNames="id"
constraintName="fk_m_address_client_id"
onDelete="SET NULL" onUpdate="CASCADE"/>
</changeSet>
<changeSet author="fineract" id="0245-3" context="default">
<createIndex tableName="m_address" indexName="idx_m_address_client_id">
<column name="client_id"/>
</createIndex>
</changeSet>
<!-- Backfill client_id from m_client_address join table for existing records -->
<changeSet author="fineract" id="0245-4" context="default">
<sql>
UPDATE m_address
SET client_id = ca.client_id
FROM m_client_address ca
WHERE m_address.id = ca.address_id
AND m_address.client_id IS NULL;
</sql>
</changeSet>
<!-- Seed ADDRESS_TYPE code values (code id=29) -->
<changeSet author="fineract" id="0245-5" context="default">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_code_value WHERE code_id = 29
</sqlCheck>
</preConditions>
<insert tableName="m_code_value">
<column name="code_id" valueNumeric="29"/>
<column name="code_value" value="Permanent Address"/>
<column name="order_position" valueNumeric="1"/>
<column name="code_description" value="Permanent / Home address"/>
<column name="is_active" valueBoolean="true"/>
<column name="is_mandatory" valueBoolean="false"/>
</insert>
<insert tableName="m_code_value">
<column name="code_id" valueNumeric="29"/>
<column name="code_value" value="Current / Residential"/>
<column name="order_position" valueNumeric="2"/>
<column name="code_description" value="Current residential address"/>
<column name="is_active" valueBoolean="true"/>
<column name="is_mandatory" valueBoolean="false"/>
</insert>
<insert tableName="m_code_value">
<column name="code_id" valueNumeric="29"/>
<column name="code_value" value="Office / Work"/>
<column name="order_position" valueNumeric="3"/>
<column name="code_description" value="Office or workplace address"/>
<column name="is_active" valueBoolean="true"/>
<column name="is_mandatory" valueBoolean="false"/>
</insert>
<insert tableName="m_code_value">
<column name="code_id" valueNumeric="29"/>
<column name="code_value" value="Correspondence"/>
<column name="order_position" valueNumeric="4"/>
<column name="code_description" value="Correspondence / mailing address"/>
<column name="is_active" valueBoolean="true"/>
<column name="is_mandatory" valueBoolean="false"/>
</insert>
</changeSet>
<!-- Enable townVillage, countyDistrict and restore street in field configuration -->
<changeSet author="fineract" id="0245-6" context="default">
<sql>
UPDATE m_field_configuration SET is_enabled = true WHERE field = 'townVillage' AND entity = 'ADDRESS';
UPDATE m_field_configuration SET is_enabled = true WHERE field = 'countyDistrict' AND entity = 'ADDRESS';
</sql>
</changeSet>
<changeSet author="fineract" id="0245-7" context="default">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM m_field_configuration WHERE entity = 'ADDRESS' AND field = 'street'
</sqlCheck>
</preConditions>
<insert tableName="m_field_configuration">
<column name="entity" value="ADDRESS"/>
<column name="subentity" value="CLIENT"/>
<column name="field" value="street"/>
<column name="is_enabled" valueBoolean="true"/>
<column name="is_mandatory" valueBoolean="false"/>
<column name="validation_regex" value=""/>
</insert>
</changeSet>
<!-- Add address_type_id FK column to m_address -->
<changeSet author="fineract" id="0245-8" context="default">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="m_address" columnName="address_type_id"/></not>
</preConditions>
<addColumn tableName="m_address">
<column name="address_type_id" type="INT">
<constraints nullable="true"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="0245-9" context="default">
<preConditions onFail="MARK_RAN">
<not><foreignKeyConstraintExists foreignKeyName="fk_m_address_address_type_id"/></not>
</preConditions>
<addForeignKeyConstraint baseTableName="m_address" baseColumnNames="address_type_id"
referencedTableName="m_code_value" referencedColumnNames="id"
constraintName="fk_m_address_address_type_id"/>
</changeSet>
<!-- Backfill address_type_id from m_client_address -->
<changeSet author="fineract" id="0245-10" context="default">
<sql>
UPDATE m_address
SET address_type_id = ca.address_type_id
FROM m_client_address ca
WHERE m_address.id = ca.address_id
AND m_address.address_type_id IS NULL;
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0246-0" context="mysql,mariadb,postgresql">
<comment>Widen pan_aadhaar_seeding_status from VARCHAR(5) to VARCHAR(20) to support LINKED/NOT_LINKED values</comment>
<modifyDataType tableName="m_client_kyc" columnName="pan_aadhaar_seeding_status" newDataType="VARCHAR(20)"/>
</changeSet>
<changeSet author="fineract" id="0246-1" context="mysql,mariadb,postgresql">
<comment>Add extended KYC fields: enterprise/non-individual, KYC document, AML/compliance, account status, Form 60/15</comment>
<!-- Enterprise / Non-Individual fields -->
<addColumn tableName="m_client_kyc">
<column name="enterprise_pan" type="VARCHAR(10)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="cin_number" type="VARCHAR(21)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="incorporation_date" type="DATE"/>
</addColumn>
<!-- KYC Document -->
<addColumn tableName="m_client_kyc">
<column name="kyc_document_name" type="VARCHAR(100)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="kyc_document_number" type="VARCHAR(50)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="kyc_doc_issue_date" type="DATE"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="kyc_doc_expiry_date" type="DATE"/>
</addColumn>
<!-- AML / Compliance flags -->
<addColumn tableName="m_client_kyc">
<column name="aml_check_status" type="VARCHAR(20)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="banned_entity_match" type="VARCHAR(10)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="willful_defaulter" type="VARCHAR(10)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="ecgc_sal_flag" type="VARCHAR(10)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="dormant_flag" type="VARCHAR(20)"/>
</addColumn>
<!-- Account status / Freeze -->
<addColumn tableName="m_client_kyc">
<column name="account_status" type="VARCHAR(20)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="freeze_reason" type="VARCHAR(200)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="freeze_authority" type="VARCHAR(100)"/>
</addColumn>
<!-- Form 60 / Form 15G/15H -->
<addColumn tableName="m_client_kyc">
<column name="form60_type" type="VARCHAR(20)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="form60_date" type="DATE"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="form15_applicable" type="BOOLEAN"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="form15_type" type="VARCHAR(10)"/>
</addColumn>
<addColumn tableName="m_client_kyc">
<column name="form15_fy" type="VARCHAR(10)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0247-1" context="default">
<insert tableName="m_code_value">
<column name="code_id" valueNumeric="17"/>
<column name="code_value" value="HUF (Hindu Undivided Family)"/>
<column name="code_description" value="Hindu Undivided Family"/>
<column name="order_position" valueNumeric="1"/>
<column name="is_active" valueBoolean="true"/>
<column name="is_mandatory" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0248-1" context="default">
<addColumn tableName="m_client_kyc">
<column name="cin_verified" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="cin_verified_on" type="date"/>
<column name="din_number" type="varchar(8)"/>
<column name="din_verified" type="boolean" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="din_verified_on" type="date"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.13.xsd">
<changeSet author="fineract" id="0249-1" context="default">
<addColumn tableName="m_client_non_person">
<column name="authorized_signatory" type="varchar(500)"/>
<column name="additional_info" type="varchar(500)"/>
<column name="date_of_registration" type="date"/>
</addColumn>
</changeSet>
</databaseChangeLog>