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();
}
/** 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. */
public void markVerified(final String ckycNumber) {
this.kycStatus = ClientKycStatus.VERIFIED.getValue();
@@ -184,7 +184,7 @@ public class AddressReadPlatformServiceImpl implements AddressReadPlatformServic
public List<AddressData> retrieveAllClientAddress(final long clientid) {
this.context.authenticatedUser();
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
}
@@ -193,7 +193,7 @@ public class AddressReadPlatformServiceImpl implements AddressReadPlatformServic
this.context.authenticatedUser();
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
}
@@ -22,6 +22,7 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
@@ -62,12 +63,16 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
final CodeValue addressTypeIdCodeValue = codeValueRepository.getReferenceById(addressTypeId);
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);
address.setClient(client);
address.setAddressType(addressTypeIdCodeValue);
addressRepository.save(address);
final ClientAddress clientAddress = createClientAddress(client, jsonObject, addressTypeIdCodeValue, address);
clientAddress.setIs_active(true);
clientAddressRepository.saveAndFlush(clientAddress);
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();
}
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) {
boolean clientAddressIsActive = false;
if (jsonObject.get("isActive") != null) {
@@ -240,14 +254,22 @@ public class AddressWritePlatformServiceImpl implements AddressWritePlatformServ
if (is_address_update) {
addobj.setUpdatedOn(LocalDate.now(DateUtils.getDateTimeZoneOfTenant()));
this.addressRepository.save(addobj);
}
final Boolean testActive = command.booleanPrimitiveValueOfParameterNamed("isActive");
if (testActive != null) {
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);
} 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();
}
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.client.domain;
import java.util.List;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.springframework.data.jpa.repository.JpaRepository;
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> {
@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,
@Param("isActive") boolean isActive);
ClientAddress findOneByClientIdAndAddressTypeAndIsActive(@Param("clientId") long clientId,
@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 ")
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,
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) {
@@ -23,13 +23,17 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import jakarta.persistence.PersistenceException;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
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.CommandProcessingResultBuilder;
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.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -118,15 +122,27 @@ public class ClientKycWritePlatformServiceJpaRepositoryImpl implements ClientKyc
final ClientKyc kyc = kycRepositoryWrapper.findOneWithNotFoundDetection(kycId);
final ClientKycStatus currentStatus = kyc.getKycStatus();
// When re-editing a VERIFIED record, remarks are mandatory (audit trail)
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);
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
if (!currentStatus.isInProgress()) {
if (!kyc.getKycStatus().isInProgress()) {
kyc.markInProgress();
changes.put("kycStatus", ClientKycStatus.IN_PROGRESS.getValue());
}