From aabda18b69ad30cda6fe0c1f5ad4563657c05bb8 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 28 Jul 2026 12:37:24 +0530 Subject: [PATCH] feat(clients): enforce Mother + Father/Caretaker family members on client activation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CARETAKER as a new RELATIONSHIP code value (previously missing — a client with no father had no valid way to satisfy the rule below). - Add ClientFamilyMembersWritePlatformService.validateFamilyRequirementsForActivation(), called from activateClient(): requires an active "Mother" family member, plus either "Father" or "Caretaker", for individual clients (non-individual entities are exempt). - Two distinct exceptions (ClientFamilyMemberMotherRequiredException / ClientFamilyMemberFatherOrCaretakerRequiredException) so the two failure reasons surface as separate errors instead of one merged message, mirroring the frontend's separate banners. Co-Authored-By: Claude Sonnet 5 --- .../domain/ClientFamilyMembersRepository.java | 8 ++++ ...berFatherOrCaretakerRequiredException.java | 34 ++++++++++++++ ...ntFamilyMemberMotherRequiredException.java | 34 ++++++++++++++ ...ientFamilyMembersWritePlatformService.java | 8 ++++ ...FamilyMembersWritePlatformServiceImpl.java | 44 ++++++++++++++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 1 + .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0427_add_caretaker_relationship_value.xml | 45 +++++++++++++++++++ 8 files changed, 175 insertions(+) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberFatherOrCaretakerRequiredException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberMotherRequiredException.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0427_add_caretaker_relationship_value.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientFamilyMembersRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientFamilyMembersRepository.java index e9614d5bd..9276827f9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientFamilyMembersRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/ClientFamilyMembersRepository.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.client.domain; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; @@ -52,4 +53,11 @@ public interface ClientFamilyMembersRepository @Param("relationshipId") Long relationshipId, @Param("excludeFamilyMemberId") Long excludeFamilyMemberId); + /** + * All non-deleted family members for a client, with their relationship CodeValue. Backs the + * "Mother required, plus Father or Caretaker" activation gate. + */ + @Query("SELECT fm FROM ClientFamilyMembers fm WHERE fm.client.id = :clientId AND fm.isDeleted = false") + List findAllActiveByClient(@Param("clientId") Long clientId); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberFatherOrCaretakerRequiredException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberFatherOrCaretakerRequiredException.java new file mode 100644 index 000000000..1f6b37de5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberFatherOrCaretakerRequiredException.java @@ -0,0 +1,34 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown on client activation when neither an active "Father" nor "Caretaker" family member has + * been recorded. Kept separate from {@link ClientFamilyMemberMotherRequiredException} so the two + * failure reasons surface as distinct errors instead of one merged message. + */ +public class ClientFamilyMemberFatherOrCaretakerRequiredException extends AbstractPlatformDomainRuleException { + + public ClientFamilyMemberFatherOrCaretakerRequiredException() { + super("error.msg.client.family.member.father.or.caretaker.required", + "Either Father or a Caretaker is required as a family member before this client can be activated."); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberMotherRequiredException.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberMotherRequiredException.java new file mode 100644 index 000000000..54fb99532 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/exception/ClientFamilyMemberMotherRequiredException.java @@ -0,0 +1,34 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +/** + * Thrown on client activation when no active "Mother" family member has been recorded. Kept + * separate from {@link ClientFamilyMemberFatherOrCaretakerRequiredException} so the two failure + * reasons surface as distinct errors instead of one merged message. + */ +public class ClientFamilyMemberMotherRequiredException extends AbstractPlatformDomainRuleException { + + public ClientFamilyMemberMotherRequiredException() { + super("error.msg.client.family.member.mother.required", + "Mother is required as a family member before this client can be activated."); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformService.java index 9b7dd7edb..54da8b18a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformService.java @@ -33,4 +33,12 @@ public interface ClientFamilyMembersWritePlatformService { CommandProcessingResult deleteFamilyMember(Long familyMemberId, JsonCommand command); + /** + * Enforces that an individual client has "Mother" plus either "Father" or "Caretaker" recorded + * as active family members. No-op for non-individual (entity) clients. Throws + * {@code ClientFamilyMemberMotherRequiredException} or + * {@code ClientFamilyMemberFatherOrCaretakerRequiredException} on failure. + */ + void validateFamilyRequirementsForActivation(Client client); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java index 1e9d342d9..8fa38b8c8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientFamilyMembersWritePlatformServiceImpl.java @@ -37,10 +37,13 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import java.util.List; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.ClientFamilyMembers; import org.apache.fineract.portfolio.client.domain.ClientFamilyMembersRepository; import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.fineract.portfolio.client.exception.ClientFamilyMemberFatherOrCaretakerRequiredException; +import org.apache.fineract.portfolio.client.exception.ClientFamilyMemberMotherRequiredException; import org.apache.fineract.portfolio.client.serialization.ClientFamilyMemberCommandFromApiJsonDeserializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,6 +134,47 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily } } + @Override + public void validateFamilyRequirementsForActivation(final Client client) { + if (client == null || client.getId() == null || client.getLegalForm() == null) { + return; + } + // Family (Mother/Father/Caretaker) is an individual-client concept — see + // assertCanHaveFamilyMembers above; non-individual entities are exempt from this gate. + if (org.apache.fineract.portfolio.client.domain.LegalForm.fromInt(client.getLegalForm()).isEntity()) { + return; + } + + final List members = clientFamilyRepository.findAllActiveByClient(client.getId()); + boolean hasMother = false; + boolean hasFather = false; + boolean hasCaretaker = false; + for (final ClientFamilyMembers member : members) { + final CodeValue relationship = member.getRelationship(); + if (relationship == null || relationship.getLabel() == null) { + continue; + } + final String label = relationship.getLabel().trim().toLowerCase(Locale.ROOT); + switch (label) { + case "mother" -> hasMother = true; + case "father" -> hasFather = true; + case "caretaker" -> hasCaretaker = true; + default -> { + // other relationships don't affect this rule + } + } + } + + // Reported as two independent failures rather than one merged message, so the caller can + // tell which specific requirement is unmet. + if (!hasMother) { + throw new ClientFamilyMemberMotherRequiredException(); + } + if (!hasFather && !hasCaretaker) { + throw new ClientFamilyMemberFatherOrCaretakerRequiredException(); + } + } + @Override public CommandProcessingResult addFamilyMember(final long clientId, final JsonCommand command) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java index 9c2947167..fc3a80a35 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/service/ClientWritePlatformServiceJpaRepositoryImpl.java @@ -1449,6 +1449,7 @@ public class ClientWritePlatformServiceJpaRepositoryImpl implements ClientWriteP final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId, true); validateParentGroupRulesBeforeClientActivation(client); + this.clientFamilyMembersWritePlatformService.validateFamilyRequirementsForActivation(client); final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); final LocalDate activationDate = command.localDateValueOfParameterNamed("activationDate"); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 0177a5aa6..11778358a 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -447,6 +447,7 @@ + + + + + + + + SELECT COUNT(*) FROM m_code_value cv JOIN m_code c ON c.id = cv.code_id + WHERE c.code_name = 'RELATIONSHIP' AND cv.code_value = 'CARETAKER' + + + + INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory) + SELECT c.id, 'CARETAKER', + (SELECT COALESCE(MAX(cv.order_position), 0) FROM m_code_value cv WHERE cv.code_id = c.id) + 1, + true, false + FROM m_code c + WHERE c.code_name = 'RELATIONSHIP'; + + + +