From 011239f3995f9ea94a893ed4a60251a3b154564c Mon Sep 17 00:00:00 2001 From: Aditya Date: Tue, 7 Apr 2026 15:00:37 +0530 Subject: [PATCH] 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) --- .../portfolio/client/domain/ClientKyc.java | 13 ++++++++++ .../AddressReadPlatformServiceImpl.java | 4 ++-- .../AddressWritePlatformServiceImpl.java | 24 ++++++++++++++++++- .../domain/ClientAddressRepository.java | 9 +++++-- .../ClientAddressRepositoryWrapper.java | 2 +- ...WritePlatformServiceJpaRepositoryImpl.java | 20 ++++++++++++++-- 6 files changed, 64 insertions(+), 8 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientKyc.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientKyc.java index 7deb9974d..9c3764224 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientKyc.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/client/domain/ClientKyc.java @@ -414,6 +414,19 @@ public class ClientKyc extends AbstractAuditableWithUTCDateTimeCustom { 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(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressReadPlatformServiceImpl.java index 249d60c1c..61829f500 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressReadPlatformServiceImpl.java @@ -184,7 +184,7 @@ public class AddressReadPlatformServiceImpl implements AddressReadPlatformServic public List 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 } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java index f7172ee7e..316c86bc0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/address/service/AddressWritePlatformServiceImpl.java @@ -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 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(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepository.java index 5292bd2a9..2611419c7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepository.java @@ -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, JpaSpecificationExecutor { @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 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepositoryWrapper.java index f1b139e86..b61771307 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepositoryWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientAddressRepositoryWrapper.java @@ -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) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientKycWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientKycWritePlatformServiceJpaRepositoryImpl.java index d5aed7d98..2d6e3c01f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientKycWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientKycWritePlatformServiceJpaRepositoryImpl.java @@ -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 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()); }