Fix KYC update on verified records and client address duplication

- Allow KYC updates on VERIFIED records with mandatory remarks for audit trail;
  reset PAN verification when PAN number changes, moving status to IN_PROGRESS
- Fix client address duplicates: deactivate old addresses of same type on
  create/update, persist isActive flag (missing save), filter reads by is_active

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-07 15:00:37 +05:30
co-authored by Claude Opus 4.6
parent 87e132f93d
commit 011239f399
6 changed files with 64 additions and 8 deletions
@@ -414,6 +414,19 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue(); this.kycStatus = ClientKycStatus.IN_PROGRESS.getValue();
} }
/** Resets PAN verification flags so that re-verification is required (e.g. after DOB or PAN edit). */
public void resetPanVerification() {
this.panVerified = false;
this.panVerifiedOn = null;
this.panFullName = null;
this.panFirstName = null;
this.panLastName = null;
this.panCategory = null;
this.panStatus = null;
this.verifiedOn = null;
this.expiryDate = null;
}
/** Called after successful 3rd-party verification of all required documents. */ /** Called after successful 3rd-party verification of all required documents. */
public void markVerified(final String ckycNumber) { public void markVerified(final String ckycNumber) {
this.kycStatus = ClientKycStatus.VERIFIED.getValue(); this.kycStatus = ClientKycStatus.VERIFIED.getValue();
@@ -184,7 +184,7 @@ public class AddressReadPlatformServiceImpl implements AddressReadPlatformServic
public List<AddressData> retrieveAllClientAddress(final long clientid) { public List<AddressData> retrieveAllClientAddress(final long clientid) {
this.context.authenticatedUser(); this.context.authenticatedUser();
final AddMapper rm = new AddMapper(); final AddMapper rm = new AddMapper();
final String sql = "select " + rm.schema() + " and ca.client_id=?"; final String sql = "select " + rm.schema() + " and ca.client_id=? and ca.is_active=true";
return this.jdbcTemplate.query(sql, rm, new Object[] { clientid }); // NOSONAR return this.jdbcTemplate.query(sql, rm, new Object[] { clientid }); // NOSONAR
} }
@@ -193,7 +193,7 @@ public class AddressReadPlatformServiceImpl implements AddressReadPlatformServic
this.context.authenticatedUser(); this.context.authenticatedUser();
final AddMapper rm = new AddMapper(); final AddMapper rm = new AddMapper();
final String sql = "select " + rm.schema() + " and ca.client_id=? and ca.address_type_id=?"; final String sql = "select " + rm.schema() + " and ca.client_id=? and ca.address_type_id=? and ca.is_active=true";
return this.jdbcTemplate.query(sql, rm, new Object[] { clientid, typeid }); // NOSONAR return this.jdbcTemplate.query(sql, rm, new Object[] { clientid, typeid }); // NOSONAR
} }
@@ -22,6 +22,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
@@ -62,12 +63,16 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId); final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId);
final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId); final Client client = clientRepositoryWrapper.findOneWithNotFoundDetection(clientId);
// Deactivate existing active addresses of the same type for this client
deactivateExistingAddresses(client.getId(), addressTypeIdCodeValue);
final Address address = createAddress(jsonObject); final Address address = createAddress(jsonObject);
address.setClient(client); address.setClient(client);
address.setAddressType(addressTypeIdCodeValue); address.setAddressType(addressTypeIdCodeValue);
addressRepository.save(address); addressRepository.save(address);
final ClientAddress clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address); final ClientAddress clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address);
clientAddress.setIs_active(true);
clientAddressRepository.saveAndFlush(clientAddress); clientAddressRepository.saveAndFlush(clientAddress);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddress.getId()).build(); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddress.getId()).build();
@@ -103,6 +108,15 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddress.getId()).build(); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddress.getId()).build();
} }
private void deactivateExistingAddresses(final Long clientId, final CodeValue addressType) {
final List<ClientAddress> existing = clientAddressRepository
.findByClientIdAndAddressTypeAndIsActive(clientId, addressType.getId(), true);
for (final ClientAddress ca : existing) {
ca.setIs_active(false);
clientAddressRepository.save(ca);
}
}
private ClientAddress createClientAddress(Client client, JsonObject jsonObject, CodeValue addressTypeIdCodeValue, Address address) { private ClientAddress createClientAddress(Client client, JsonObject jsonObject, CodeValue addressTypeIdCodeValue, Address address) {
boolean clientAddressIsActive = false; boolean clientAddressIsActive = false;
if (jsonObject.get("isActive") != null) { if (jsonObject.get("isActive") != null) {
@@ -240,14 +254,22 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
if (is_address_update) { if (is_address_update) {
addobj.setUpdatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())); addobj.setUpdatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
this.addressRepository.save(addobj); this.addressRepository.save(addobj);
} }
final Boolean testActive = command.booleanPrimitiveValueOfParameterNamed("isActive"); final Boolean testActive = command.booleanPrimitiveValueOfParameterNamed("isActive");
if (testActive != null) { if (testActive != null) {
final boolean active = command.booleanPrimitiveValueOfParameterNamed("isActive"); final boolean active = command.booleanPrimitiveValueOfParameterNamed("isActive");
if (active) {
// Deactivate other addresses of the same type for this client
deactivateExistingAddresses(clientId, clientAddressObj.getAddressType());
}
clientAddressObj.setIs_active(active); clientAddressObj.setIs_active(active);
} else {
// On update, keep current address active and deactivate others of the same type
deactivateExistingAddresses(clientId, clientAddressObj.getAddressType());
clientAddressObj.setIs_active(true);
} }
this.clientAddressRepository.saveAndFlush(clientAddressObj);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddressObj.getId()).build(); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(clientAddressObj.getId()).build();
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.client.domain; package org.apache.fineract.portfolio.client.domain;
import java.util.List;
import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -27,8 +28,12 @@ import org.springframework.data.repository.query.Param;
public interface ClientAddressRepository extends JpaRepository<ClientAddress, Long>, JpaSpecificationExecutor<ClientAddress> { public interface ClientAddressRepository extends JpaRepository<ClientAddress, Long>, JpaSpecificationExecutor<ClientAddress> {
@Query("SELECT clientAddress FROM ClientAddress clientAddress WHERE clientAddress.client.id = :clientId AND clientAddress.addressType = :addressType AND clientAddress.isActive = :isActive ") @Query("SELECT clientAddress FROM ClientAddress clientAddress WHERE clientAddress.client.id = :clientId AND clientAddress.addressType = :addressType AND clientAddress.isActive = :isActive ")
ClientAddress findByClientIdAndAddressTypeAndIsActive(@Param("clientId") long clientId, @Param("addressType") CodeValue addressType, ClientAddress findOneByClientIdAndAddressTypeAndIsActive(@Param("clientId") long clientId,
@Param("isActive") boolean isActive); @Param("addressType") CodeValue addressType, @Param("isActive") boolean isActive);
@Query("SELECT ca FROM ClientAddress ca WHERE ca.client.id = :clientId AND ca.addressType.id = :addressTypeId AND ca.isActive = :isActive")
List<ClientAddress> findByClientIdAndAddressTypeAndIsActive(@Param("clientId") long clientId,
@Param("addressTypeId") long addressTypeId, @Param("isActive") boolean isActive);
@Query("SELECT clientAddress FROM ClientAddress clientAddress WHERE clientAddress.client.id = :clientId AND clientAddress.address.id = :addressId ") @Query("SELECT clientAddress FROM ClientAddress clientAddress WHERE clientAddress.client.id = :clientId AND clientAddress.address.id = :addressId ")
ClientAddress findByClientIdAndAddressId(@Param("clientId") long clientId, @Param("addressId") long addressId); ClientAddress findByClientIdAndAddressId(@Param("clientId") long clientId, @Param("addressId") long addressId);
@@ -34,7 +34,7 @@ public class ClientAddressRepositoryWrapper {
public ClientAddress findOneByClientIdAndAddressTypeAndIsActive(final long clientId, final CodeValue addressType, public ClientAddress findOneByClientIdAndAddressTypeAndIsActive(final long clientId, final CodeValue addressType,
final boolean isActive) { final boolean isActive) {
return this.clientAddressRepository.findByClientIdAndAddressTypeAndIsActive(clientId, addressType, isActive); return this.clientAddressRepository.findOneByClientIdAndAddressTypeAndIsActive(clientId, addressType, isActive);
} }
public ClientAddress findOneByClientIdAndAddressId(final long clientId, final long addressId) { public ClientAddress findOneByClientIdAndAddressId(final long clientId, final long addressId) {
@@ -23,13 +23,17 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import jakarta.persistence.PersistenceException; import jakarta.persistence.PersistenceException;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler; import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -118,15 +122,27 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId); final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
final ClientKycStatus currentStatus = kyc.getKycStatus(); final ClientKycStatus currentStatus = kyc.getKycStatus();
// When re-editing a VERIFIED record, remarks are mandatory (audit trail)
if (currentStatus.isVerified()) { if (currentStatus.isVerified()) {
throw new ClientKycInvalidStateTransitionException("update", currentStatus.name()); final String remarks = command.stringValueOfParameterNamed("remarks");
if (StringUtils.isBlank(remarks)) {
throw new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(
"error.msg.client.kyc.remarks.required.for.reverification",
"Remarks are mandatory when updating a verified KYC record.", "remarks")));
}
} }
final Map<String, Object> changes = kyc.update(command); final Map<String, Object> changes = kyc.update(command);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
// If PAN-related fields changed on a VERIFIED record, reset PAN verification
if (currentStatus.isVerified() && changes.containsKey("panNumber")) {
kyc.resetPanVerification();
changes.put("panVerified", false);
}
// Move to IN_PROGRESS when documents are updated // Move to IN_PROGRESS when documents are updated
if (!currentStatus.isInProgress()) { if (!kyc.getKycStatus().isInProgress()) {
kyc.markInProgress(); kyc.markInProgress();
changes.put("kycStatus", ClientKycStatus.IN_PROGRESS.getValue()); changes.put("kycStatus", ClientKycStatus.IN_PROGRESS.getValue());
} }