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:
co-authored by
Claude Opus 4.6
parent
87e132f93d
commit
011239f399
+13
@@ -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();
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -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);
|
||||||
|
|||||||
+1
-1
@@ -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) {
|
||||||
|
|||||||
+18
-2
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user