This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 76d108953b FINERACT-2498: Add GROUP as a Guarantor Type to Support 
Group Savings Accounts as Loan Collateral
76d108953b is described below

commit 76d108953bcc4e696d0dbbfaf33857ef878dfb07
Author: Ralph Hopman <[email protected]>
AuthorDate: Thu Feb 19 18:18:35 2026 +0100

    FINERACT-2498: Add GROUP as a Guarantor Type to Support Group Savings 
Accounts as Loan Collateral
---
 .../guarantor/domain/GuarantorType.java            |   7 +-
 .../constants/TemplatePopulateImportConstants.java |   1 +
 .../guarantor/GuarantorImportHandler.java          |   2 +
 .../guarantor/GuarantorWorkbookPopulator.java      |   5 +-
 .../loanaccount/guarantor/data/GuarantorData.java  |   4 +
 .../loanaccount/guarantor/domain/Guarantor.java    |   4 +
 .../service/GuarantorDomainServiceImpl.java        |   2 +-
 ...ntorWritePlatformServiceJpaRepositoryIImpl.java |  22 +-
 .../GroupSavingsIntegrationTest.java               | 318 ++++++++++++++++++++-
 .../guarantor/GuarantorHelper.java                 |  19 ++
 .../guarantor/GuarantorTestBuilder.java            |  12 +-
 11 files changed, 387 insertions(+), 9 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/GuarantorType.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/GuarantorType.java
index 831f39fc1d..966274a4c5 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/GuarantorType.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/GuarantorType.java
@@ -25,7 +25,8 @@ public enum GuarantorType {
 
     CUSTOMER(1, "guarantor.existing.customer"), //
     STAFF(2, "guarantor.staff"), //
-    EXTERNAL(3, "guarantor.external"); //
+    EXTERNAL(3, "guarantor.external"), //
+    GROUP(4, "guarantor.existing.group"); //
 
     private final Integer value;
     private final String code;
@@ -90,4 +91,8 @@ public enum GuarantorType {
         return this.value.equals(GuarantorType.STAFF.getValue());
     }
 
+    public boolean isGroup() {
+        return this.value.equals(GuarantorType.GROUP.getValue());
+    }
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
index a93f674073..c7226e1c77 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/constants/TemplatePopulateImportConstants.java
@@ -141,6 +141,7 @@ public final class TemplatePopulateImportConstants {
     // Guarantor Types
     public static final String GUARANTOR_INTERNAL = "Internal";
     public static final String GUARANTOR_EXTERNAL = "External";
+    public static final String GUARANTOR_GROUP = "Group";
 
     // Loan Account/Loan repayment Client External Id
     public static final Boolean CONTAINS_CLIENT_EXTERNAL_ID = true;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
index e8b8c8c8b6..c51be9c638 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/guarantor/GuarantorImportHandler.java
@@ -94,6 +94,8 @@ public class GuarantorImportHandler implements ImportHandler {
                 guarantorTypeId = 1;
             } else if 
(guarantorType.equalsIgnoreCase(TemplatePopulateImportConstants.GUARANTOR_EXTERNAL))
 {
                 guarantorTypeId = 3;
+            } else if 
(guarantorType.equalsIgnoreCase(TemplatePopulateImportConstants.GUARANTOR_GROUP))
 {
+                guarantorTypeId = 4;
             }
         }
         String clientName = 
ImportHandlerUtils.readAsString(GuarantorConstants.ENTITY_ID_COL, row);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
index 984bee37d1..ab9326b68a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/populator/guarantor/GuarantorWorkbookPopulator.java
@@ -209,8 +209,9 @@ public class GuarantorWorkbookPopulator extends 
AbstractWorkbookPopulator {
                 
"INDIRECT(CONCATENATE(\"Account_\",SUBSTITUTE(SUBSTITUTE(SUBSTITUTE($B1,\" 
\",\"_\"),\"(\",\"_\"),\")\",\"_\")))");
         DataValidationConstraint savingsaccountNumberConstraint = 
validationHelper.createFormulaListConstraint(
                 
"INDIRECT(CONCATENATE(\"SavingsAccount_\",SUBSTITUTE(SUBSTITUTE(SUBSTITUTE($G1,\"
 \",\"_\"),\"(\",\"_\"),\")\",\"_\")))");
-        DataValidationConstraint guranterTypeConstraint = 
validationHelper.createExplicitListConstraint(
-                new String[] { 
TemplatePopulateImportConstants.GUARANTOR_INTERNAL, 
TemplatePopulateImportConstants.GUARANTOR_EXTERNAL });
+        DataValidationConstraint guranterTypeConstraint = validationHelper
+                .createExplicitListConstraint(new String[] { 
TemplatePopulateImportConstants.GUARANTOR_INTERNAL,
+                        TemplatePopulateImportConstants.GUARANTOR_EXTERNAL, 
TemplatePopulateImportConstants.GUARANTOR_GROUP });
         DataValidationConstraint guarantorRelationshipConstraint = 
validationHelper.createFormulaListConstraint("GuarantorRelationship");
         DataValidationConstraint entityofficeNameConstraint = 
validationHelper.createFormulaListConstraint("Office");
         DataValidationConstraint entityclientNameConstraint = validationHelper
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java
index 4bdd36bea3..0aa9a33bdb 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/data/GuarantorData.java
@@ -207,4 +207,8 @@ public class GuarantorData implements IGuarantor {
     public boolean isStaffMember() {
         return 
GuarantorType.STAFF.getValue().equals(this.guarantorType.getId().intValue());
     }
+
+    public boolean isExistingGroup() {
+        return 
GuarantorType.GROUP.getValue().equals(this.guarantorType.getId().intValue());
+    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/Guarantor.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/Guarantor.java
index 8b734bb764..7f7906f0fa 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/Guarantor.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/domain/Guarantor.java
@@ -188,6 +188,10 @@ public class Guarantor extends 
AbstractPersistableCustom<Long> {
         return GuarantorType.STAFF.getValue().equals(this.gurantorType);
     }
 
+    public boolean isExistingGroup() {
+        return GuarantorType.GROUP.getValue().equals(this.gurantorType);
+    }
+
     public boolean isExternalGuarantor() {
         return GuarantorType.EXTERNAL.getValue().equals(this.gurantorType);
     }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java
index 4113a842d4..82d532ad75 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorDomainServiceImpl.java
@@ -390,7 +390,7 @@ public class GuarantorDomainServiceImpl implements 
GuarantorDomainService {
                         if (guarantor.isSelfGuarantee()) {
                             selfGuarantorList.add(guarantorFundingDetails);
                             selfGuarantee = 
selfGuarantee.add(guarantorFundingDetails.getAmountRemaining());
-                        } else if (guarantor.isExistingCustomer()) {
+                        } else if (guarantor.isExistingCustomer() || 
guarantor.isExistingGroup()) {
                             externalGuarantorList.add(guarantorFundingDetails);
                             guarantorGuarantee = 
guarantorGuarantee.add(guarantorFundingDetails.getAmountRemaining());
                         }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java
index 7e71bd14c1..9773a0e573 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/guarantor/service/GuarantorWritePlatformServiceJpaRepositoryIImpl.java
@@ -38,6 +38,7 @@ import 
org.apache.fineract.portfolio.account.domain.AccountAssociationType;
 import org.apache.fineract.portfolio.account.domain.AccountAssociations;
 import 
org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository;
 import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
+import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
 import org.apache.fineract.portfolio.loanaccount.guarantor.GuarantorConstants;
@@ -70,6 +71,7 @@ public class GuarantorWritePlatformServiceJpaRepositoryIImpl 
implements Guaranto
 
     private final ClientRepositoryWrapper clientRepositoryWrapper;
     private final StaffRepositoryWrapper staffRepositoryWrapper;
+    private final GroupRepositoryWrapper groupRepositoryWrapper;
     private final LoanRepositoryWrapper loanRepositoryWrapper;
     private final GuarantorRepository guarantorRepository;
     private final GuarantorCommandFromApiJsonDeserializer 
fromApiJsonDeserializer;
@@ -81,11 +83,13 @@ public class 
GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
     @Autowired
     public GuarantorWritePlatformServiceJpaRepositoryIImpl(final 
LoanRepositoryWrapper loanRepositoryWrapper,
             final GuarantorRepository guarantorRepository, final 
ClientRepositoryWrapper clientRepositoryWrapper,
-            final StaffRepositoryWrapper staffRepositoryWrapper, final 
GuarantorCommandFromApiJsonDeserializer fromApiJsonDeserializer,
+            final StaffRepositoryWrapper staffRepositoryWrapper, final 
GroupRepositoryWrapper groupRepositoryWrapper,
+            final GuarantorCommandFromApiJsonDeserializer 
fromApiJsonDeserializer,
             final CodeValueRepositoryWrapper codeValueRepositoryWrapper, final 
SavingsAccountAssembler savingsAccountAssembler,
             final AccountAssociationsRepository accountAssociationsRepository, 
final GuarantorDomainService guarantorDomainService) {
         this.loanRepositoryWrapper = loanRepositoryWrapper;
         this.clientRepositoryWrapper = clientRepositoryWrapper;
+        this.groupRepositoryWrapper = groupRepositoryWrapper;
         this.fromApiJsonDeserializer = fromApiJsonDeserializer;
         this.guarantorRepository = guarantorRepository;
         this.staffRepositoryWrapper = staffRepositoryWrapper;
@@ -148,6 +152,8 @@ public class 
GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
                         String defaultUserMessage = null;
                         if 
(guarantorTypeId.equals(GuarantorType.STAFF.getValue())) {
                             defaultUserMessage = 
this.staffRepositoryWrapper.findOneWithNotFoundDetection(entityId).displayName();
+                        } else if 
(guarantorTypeId.equals(GuarantorType.GROUP.getValue())) {
+                            defaultUserMessage = 
this.groupRepositoryWrapper.findOneWithNotFoundDetection(entityId).getName();
                         } else {
                             defaultUserMessage = 
this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId).getDisplayName();
                         }
@@ -225,12 +231,19 @@ public class 
GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
             final List<Guarantor> existGuarantorList = 
this.guarantorRepository.findByLoan(loan);
             final Integer guarantorTypeId = 
guarantorCommand.getGuarantorTypeId();
             final GuarantorType guarantorType = 
GuarantorType.fromInt(guarantorTypeId);
-            if (guarantorType.isCustomer() || guarantorType.isStaff()) {
+            if (guarantorType.isCustomer() || guarantorType.isStaff() || 
guarantorType.isGroup()) {
                 final Long entityId = guarantorCommand.getEntityId();
                 for (final Guarantor guarantor : existGuarantorList) {
                     if (guarantor.getEntityId().equals(entityId) && 
guarantor.getGurantorType().equals(guarantorTypeId)
                             && 
!guarantorForUpdate.getId().equals(guarantor.getId())) {
-                        String defaultUserMessage = 
this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId).getDisplayName();
+                        String defaultUserMessage = null;
+                        if 
(guarantorTypeId.equals(GuarantorType.STAFF.getValue())) {
+                            defaultUserMessage = 
this.staffRepositoryWrapper.findOneWithNotFoundDetection(entityId).displayName();
+                        } else if 
(guarantorTypeId.equals(GuarantorType.GROUP.getValue())) {
+                            defaultUserMessage = 
this.groupRepositoryWrapper.findOneWithNotFoundDetection(entityId).getName();
+                        } else {
+                            defaultUserMessage = 
this.clientRepositoryWrapper.findOneWithNotFoundDetection(entityId).getDisplayName();
+                        }
                         defaultUserMessage = defaultUserMessage + " is already 
exist as a guarantor for this loan";
                         final String action = loan.client() != null ? 
"client.guarantor" : "group.guarantor";
                         throw new DuplicateGuarantorException(action, 
"is.already.exist.same.loan", defaultUserMessage, entityId, loanId);
@@ -336,6 +349,9 @@ public class 
GuarantorWritePlatformServiceJpaRepositoryIImpl implements Guaranto
 
         } else if (guarantor.isExistingEmployee()) {
             
this.staffRepositoryWrapper.findOneWithNotFoundDetection(guarantor.getEntityId());
+        } else if (guarantor.isExistingGroup()) {
+            // check group exists
+            
this.groupRepositoryWrapper.findOneWithNotFoundDetection(guarantor.getEntityId());
         }
     }
 
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
index 2c6424bf59..07999a1cfb 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
@@ -957,7 +957,7 @@ public class GroupSavingsIntegrationTest {
         // Add the group savings account as guarantor collateral for the loan
         // Use GUARANTEE_AMOUNT (500) as guarantee amount (less than the 
MINIMUM_OPENING_BALANCE of 1000)
         String guarantorJSON = new GuarantorTestBuilder()
-                .existingCustomerWithGuaranteeAmount(String.valueOf(groupID), 
String.valueOf(savingsId), GUARANTEE_AMOUNT).build();
+                .existingGroupWithGuaranteeAmount(String.valueOf(groupID), 
String.valueOf(savingsId), GUARANTEE_AMOUNT).build();
 
         LOG.info("Guarantor JSON: {}", guarantorJSON);
         LOG.info("Loan ID: {}, Group ID: {}, Savings ID: {}", loanID, groupID, 
savingsId);
@@ -1023,6 +1023,322 @@ public class GroupSavingsIntegrationTest {
                 "Should find at least one on-hold transaction with 
savingsClientName populated (group name)");
     }
 
+    /**
+     * Test that creating a group guarantor with an invalid group ID fails 
with appropriate error
+     */
+    @Test
+    public void testGroupGuarantorWithInvalidGroupId() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create a client who will take out the loan
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, 
this.responseSpec);
+        Assertions.assertNotNull(clientID);
+
+        // Create loan product with hold funds
+        LoanProductTestBuilder loanProductBuilder = new 
LoanProductTestBuilder().withPrincipal(PRINCIPAL).withNumberOfRepayments("4")
+                
.withRepaymentAfterEvery("1").withRepaymentTypeAsWeek().withinterestRatePerPeriod("2")
+                
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
+                .withOnHoldFundDetails("0", "0", "0");
+        final Integer loanProductID = 
this.loanTransactionHelper.getLoanProductId(loanProductBuilder.build(null));
+        Assertions.assertNotNull(loanProductID);
+
+        // Apply for a loan
+        final String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal(PRINCIPAL).withLoanTermFrequency("4")
+                
.withLoanTermFrequencyAsWeeks().withNumberOfRepayments("4").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsWeeks()
+                
.withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+                
.withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE)
+                .build(clientID.toString(), loanProductID.toString(), null);
+        final Integer loanID = 
this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+        Assertions.assertNotNull(loanID);
+
+        // Try to create guarantor with invalid group ID (9999999)
+        final Integer invalidGroupId = 9999999;
+        String guarantorJSON = new GuarantorTestBuilder()
+                
.existingGroupWithGuaranteeAmount(String.valueOf(invalidGroupId), "1", 
GUARANTEE_AMOUNT).build();
+
+        final ResponseSpecification errorResponse = new 
ResponseSpecBuilder().build();
+        final RequestSpecification errorRequest = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        errorRequest.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+
+        ArrayList<HashMap> error = (ArrayList<HashMap>) 
this.guarantorHelper.createGuarantorWithError(loanID, guarantorJSON, 
errorRequest,
+                errorResponse);
+        // Verify we got an error response (status code may be 403 or 404 
depending on environment)
+        Assertions.assertNotNull(error, "Should return error for invalid group 
ID");
+
+        LOG.info("SUCCESS: Invalid group ID correctly rejected");
+    }
+
+    /**
+     * Test that using a client ID with GROUP guarantor type fails with 
appropriate error
+     */
+    @Test
+    public void testGroupGuarantorWithClientIdButGroupType() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create TWO clients - one for loan, one to misuse as "group"
+        final Integer loanClientID = 
ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(loanClientID);
+
+        final Integer otherClientID = 
ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(otherClientID);
+
+        // Create savings account for the other client
+        final Integer savingsProductID = 
createSavingsProduct(this.requestSpec, this.responseSpec, 
MINIMUM_OPENING_BALANCE, null, null,
+                "false");
+        final Integer clientSavingsId = 
this.savingsAccountHelper.applyForSavingsApplication(otherClientID, 
savingsProductID, "INDIVIDUAL");
+        this.savingsAccountHelper.approveSavings(clientSavingsId);
+        this.savingsAccountHelper.activateSavings(clientSavingsId);
+
+        // Create loan product
+        LoanProductTestBuilder loanProductBuilder = new 
LoanProductTestBuilder().withPrincipal(PRINCIPAL).withNumberOfRepayments("4")
+                
.withRepaymentAfterEvery("1").withRepaymentTypeAsWeek().withinterestRatePerPeriod("2")
+                
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
+                .withOnHoldFundDetails("0", "0", "0");
+        final Integer loanProductID = 
this.loanTransactionHelper.getLoanProductId(loanProductBuilder.build(null));
+
+        // Create loan
+        final String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal(PRINCIPAL).withLoanTermFrequency("4")
+                
.withLoanTermFrequencyAsWeeks().withNumberOfRepayments("4").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsWeeks()
+                
.withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+                
.withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE)
+                .build(loanClientID.toString(), loanProductID.toString(), 
null);
+        final Integer loanID = 
this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+
+        // Try to create guarantor with CLIENT ID but GROUP type (type 
mismatch)
+        String guarantorJSON = new GuarantorTestBuilder()
+                
.existingGroupWithGuaranteeAmount(String.valueOf(otherClientID), 
String.valueOf(clientSavingsId), GUARANTEE_AMOUNT).build();
+
+        final ResponseSpecification errorResponse = new 
ResponseSpecBuilder().build();
+        final RequestSpecification errorRequest = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        errorRequest.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+
+        ArrayList<HashMap> error = (ArrayList<HashMap>) 
this.guarantorHelper.createGuarantorWithError(loanID, guarantorJSON, 
errorRequest,
+                errorResponse);
+        // Verify we got an error response (status code may be 403 or 404 
depending on environment)
+        Assertions.assertNotNull(error, "Should return error for client ID 
used with GROUP type");
+
+        LOG.info("SUCCESS: Client ID with GROUP type correctly rejected");
+    }
+
+    /**
+     * Test that duplicate group guarantor detection works and shows proper 
error message with group name
+     */
+    @Test
+    public void testDuplicateGroupGuarantor() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create client for loan
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, 
this.responseSpec);
+        Assertions.assertNotNull(clientID);
+
+        // Create group with savings account
+        final Integer groupID = GroupHelper.createGroup(this.requestSpec, 
this.responseSpec, true);
+        Assertions.assertNotNull(groupID);
+
+        final Integer savingsProductID = 
createSavingsProduct(this.requestSpec, this.responseSpec, 
MINIMUM_OPENING_BALANCE, null, null,
+                "false");
+        final Integer savingsId = 
this.savingsAccountHelper.applyForSavingsApplication(groupID, savingsProductID, 
ACCOUNT_TYPE_GROUP);
+        this.savingsAccountHelper.approveSavings(savingsId);
+        this.savingsAccountHelper.activateSavings(savingsId);
+
+        // Create loan
+        LoanProductTestBuilder loanProductBuilder = new 
LoanProductTestBuilder().withPrincipal(PRINCIPAL).withNumberOfRepayments("4")
+                
.withRepaymentAfterEvery("1").withRepaymentTypeAsWeek().withinterestRatePerPeriod("2")
+                
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
+                .withOnHoldFundDetails("0", "0", "0");
+        final Integer loanProductID = 
this.loanTransactionHelper.getLoanProductId(loanProductBuilder.build(null));
+
+        final String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal(PRINCIPAL).withLoanTermFrequency("4")
+                
.withLoanTermFrequencyAsWeeks().withNumberOfRepayments("4").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsWeeks()
+                
.withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+                
.withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE)
+                .build(clientID.toString(), loanProductID.toString(), null);
+        final Integer loanID = 
this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+
+        // Add group guarantor first time - should succeed
+        String guarantorJSON = new GuarantorTestBuilder()
+                .existingGroupWithGuaranteeAmount(String.valueOf(groupID), 
String.valueOf(savingsId), GUARANTEE_AMOUNT).build();
+        Integer guarantorId1 = this.guarantorHelper.createGuarantor(loanID, 
guarantorJSON);
+        Assertions.assertNotNull(guarantorId1, "First guarantor creation 
should succeed");
+
+        // Try to add the SAME group guarantor again - should fail with 
duplicate error
+        final ResponseSpecification errorResponse = new 
ResponseSpecBuilder().expectStatusCode(403).build();
+        final RequestSpecification errorRequest = new 
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        errorRequest.header("Authorization", "Basic " + 
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+
+        ArrayList<HashMap> error = (ArrayList<HashMap>) 
this.guarantorHelper.createGuarantorWithError(loanID, guarantorJSON, 
errorRequest,
+                errorResponse);
+        Assertions.assertNotNull(error, "Should return error for duplicate 
group guarantor");
+
+        // Verify error message contains group information
+        HashMap errorData = error.get(0);
+        String userMessage = (String) 
errorData.get("userMessageGlobalisationCode");
+        Assertions.assertTrue(userMessage != null && 
userMessage.contains("already.exist"),
+                "Error message should indicate duplicate guarantor");
+
+        LOG.info("SUCCESS: Duplicate group guarantor correctly rejected");
+    }
+
+    /**
+     * Test complete loan lifecycle (approval, disbursement, repayment) with a 
group guarantor
+     */
+    @Test
+    public void testGroupGuarantorLoanLifecycle() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create client for loan
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, 
this.responseSpec);
+        Assertions.assertNotNull(clientID);
+
+        // Create group with savings account
+        final Integer groupID = GroupHelper.createGroup(this.requestSpec, 
this.responseSpec, true);
+        Assertions.assertNotNull(groupID);
+
+        final Integer savingsProductID = 
createSavingsProduct(this.requestSpec, this.responseSpec, 
MINIMUM_OPENING_BALANCE, null, null,
+                "false");
+        final Integer savingsId = 
this.savingsAccountHelper.applyForSavingsApplication(groupID, savingsProductID, 
ACCOUNT_TYPE_GROUP);
+        this.savingsAccountHelper.approveSavings(savingsId);
+        this.savingsAccountHelper.activateSavings(savingsId);
+
+        // Deposit funds into the savings account to cover the guarantee
+        this.savingsAccountHelper.depositToSavingsAccount(savingsId, 
DEPOSIT_AMOUNT, SavingsAccountHelper.TRANSACTION_DATE,
+                CommonConstants.RESPONSE_RESOURCE_ID);
+
+        // Create loan product - using minimal hold fund requirements for 
testing
+        // Focus is on verifying that group guarantors work, not on complex 
hold fund logic
+        LoanProductTestBuilder loanProductBuilder = new 
LoanProductTestBuilder().withPrincipal("2000").withNumberOfRepayments("1")
+                
.withRepaymentAfterEvery("1").withRepaymentTypeAsWeek().withinterestRatePerPeriod("0")
+                
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
+                .withOnHoldFundDetails("0", "0", "0");
+        final Integer loanProductID = 
this.loanTransactionHelper.getLoanProductId(loanProductBuilder.build(null));
+
+        final String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal("2000").withLoanTermFrequency("1")
+                
.withLoanTermFrequencyAsWeeks().withNumberOfRepayments("1").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsWeeks()
+                
.withInterestRatePerPeriod("0").withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+                
.withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE)
+                .build(clientID.toString(), loanProductID.toString(), null);
+        final Integer loanID = 
this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+
+        // Add group guarantor with amount = 1000 (50% of 2000 loan)
+        String guarantorJSON = new GuarantorTestBuilder()
+                .existingGroupWithGuaranteeAmount(String.valueOf(groupID), 
String.valueOf(savingsId), "1000").build();
+        Integer guarantorId = this.guarantorHelper.createGuarantor(loanID, 
guarantorJSON);
+        Assertions.assertNotNull(guarantorId);
+
+        // Verify guarantor was created successfully
+        ArrayList<HashMap> guarantors = 
this.guarantorHelper.getGuarantorList(loanID);
+        Assertions.assertEquals(1, guarantors.size(), "Should have 1 group 
guarantor");
+        HashMap guarantor = guarantors.get(0);
+        HashMap guarantorType = (HashMap) guarantor.get("guarantorType");
+        Assertions.assertEquals(4, guarantorType.get("id"), "Guarantor type 
should be GROUP (4)");
+
+        // Approve loan with group guarantor
+        HashMap loanStatusHashMap = 
this.loanTransactionHelper.approveLoan(SavingsAccountHelper.TRANSACTION_DATE, 
loanID);
+        Assertions.assertNotNull(loanStatusHashMap, "Loan approval should 
succeed with group guarantor");
+
+        // Disburse loan
+        this.loanTransactionHelper.disburseLoan(Long.valueOf(loanID), 
SavingsAccountHelper.TRANSACTION_DATE, 2000.0);
+
+        // Make full repayment
+        final String repaymentDate = SavingsAccountHelper.TRANSACTION_DATE;
+        this.loanTransactionHelper.makeRepayment(repaymentDate, 
Float.parseFloat("2000"), loanID);
+
+        LOG.info("SUCCESS: Group guarantor lifecycle test completed");
+    }
+
+    /**
+     * Test mixed client and group guarantors on the same loan
+     */
+    @Test
+    public void testMixedClientAndGroupGuarantors() {
+        this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
+
+        // Create loan borrower client
+        final Integer borrowerClientID = 
ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(borrowerClientID);
+
+        // Create guarantor client with savings
+        final Integer guarantorClientID = 
ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(guarantorClientID);
+
+        // Create guarantor group with savings
+        final Integer guarantorGroupID = 
GroupHelper.createGroup(this.requestSpec, this.responseSpec, true);
+        Assertions.assertNotNull(guarantorGroupID);
+
+        // Create savings accounts
+        final Integer savingsProductID = 
createSavingsProduct(this.requestSpec, this.responseSpec, 
MINIMUM_OPENING_BALANCE, null, null,
+                "false");
+
+        final Integer clientSavingsId = 
this.savingsAccountHelper.applyForSavingsApplication(guarantorClientID, 
savingsProductID,
+                "INDIVIDUAL");
+        this.savingsAccountHelper.approveSavings(clientSavingsId);
+        this.savingsAccountHelper.activateSavings(clientSavingsId);
+
+        final Integer groupSavingsId = 
this.savingsAccountHelper.applyForSavingsApplication(guarantorGroupID, 
savingsProductID,
+                ACCOUNT_TYPE_GROUP);
+        this.savingsAccountHelper.approveSavings(groupSavingsId);
+        this.savingsAccountHelper.activateSavings(groupSavingsId);
+
+        // Create loan
+        LoanProductTestBuilder loanProductBuilder = new 
LoanProductTestBuilder().withPrincipal(PRINCIPAL).withNumberOfRepayments("4")
+                
.withRepaymentAfterEvery("1").withRepaymentTypeAsWeek().withinterestRatePerPeriod("2")
+                
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance()
+                .withOnHoldFundDetails("0", "0", "0");
+        final Integer loanProductID = 
this.loanTransactionHelper.getLoanProductId(loanProductBuilder.build(null));
+
+        final String loanApplicationJSON = new 
LoanApplicationTestBuilder().withPrincipal(PRINCIPAL).withLoanTermFrequency("4")
+                
.withLoanTermFrequencyAsWeeks().withNumberOfRepayments("4").withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsWeeks()
+                
.withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                
.withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withSubmittedOnDate(SavingsAccountHelper.TRANSACTION_DATE)
+                
.withExpectedDisbursementDate(SavingsAccountHelper.TRANSACTION_DATE)
+                .build(borrowerClientID.toString(), loanProductID.toString(), 
null);
+        final Integer loanID = 
this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+
+        // Add CLIENT guarantor
+        String clientGuarantorJSON = new GuarantorTestBuilder()
+                
.existingCustomerWithGuaranteeAmount(String.valueOf(guarantorClientID), 
String.valueOf(clientSavingsId), "250").build();
+        Integer clientGuarantorId = 
this.guarantorHelper.createGuarantor(loanID, clientGuarantorJSON);
+        Assertions.assertNotNull(clientGuarantorId, "Client guarantor creation 
should succeed");
+
+        // Add GROUP guarantor
+        String groupGuarantorJSON = new GuarantorTestBuilder()
+                
.existingGroupWithGuaranteeAmount(String.valueOf(guarantorGroupID), 
String.valueOf(groupSavingsId), "250").build();
+        Integer groupGuarantorId = 
this.guarantorHelper.createGuarantor(loanID, groupGuarantorJSON);
+        Assertions.assertNotNull(groupGuarantorId, "Group guarantor creation 
should succeed");
+
+        // Retrieve all guarantors for the loan
+        ArrayList<HashMap> guarantors = 
this.guarantorHelper.getGuarantorList(loanID);
+        Assertions.assertNotNull(guarantors, "Should retrieve guarantor list");
+        Assertions.assertEquals(2, guarantors.size(), "Should have 2 
guarantors (1 client, 1 group)");
+
+        // Verify both guarantor types are present
+        boolean hasClientGuarantor = false;
+        boolean hasGroupGuarantor = false;
+
+        for (HashMap guarantor : guarantors) {
+            HashMap guarantorType = (HashMap) guarantor.get("guarantorType");
+            Integer typeId = (Integer) guarantorType.get("id");
+
+            if (typeId == 1) { // CUSTOMER/CLIENT
+                hasClientGuarantor = true;
+            } else if (typeId == 4) { // GROUP
+                hasGroupGuarantor = true;
+            }
+        }
+
+        Assertions.assertTrue(hasClientGuarantor, "Should have client 
guarantor");
+        Assertions.assertTrue(hasGroupGuarantor, "Should have group 
guarantor");
+
+        // Approve loan - both holds should be placed
+        
this.loanTransactionHelper.approveLoan(SavingsAccountHelper.TRANSACTION_DATE, 
loanID);
+
+        LOG.info("SUCCESS: Mixed client and group guarantors work together");
+    }
+
     @Test
     public void testGroupAccountAvailableBalance() {
         this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec, 
this.responseSpec);
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorHelper.java
index 6ba5a231a0..3042331435 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorHelper.java
@@ -51,6 +51,25 @@ public class GuarantorHelper {
                 CommonConstants.RESPONSE_RESOURCE_ID);
     }
 
+    // TODO: Rewrite to use fineract-client instead!
+    // Example: 
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+    // org.apache.fineract.client.models.PostLoansLoanIdRequest)
+    @Deprecated(forRemoval = true)
+    public Object createGuarantorWithError(final Integer loanId, final String 
guarantorJSON, final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec) {
+        return Utils.performServerPost(requestSpec, responseSpec, LOAN_URL + 
loanId + GUARANTOR_API_URL + TENANT, guarantorJSON,
+                CommonConstants.RESPONSE_ERROR);
+    }
+
+    // TODO: Rewrite to use fineract-client instead!
+    // Example: 
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+    // org.apache.fineract.client.models.PostLoansLoanIdRequest)
+    @Deprecated(forRemoval = true)
+    public java.util.ArrayList<HashMap> getGuarantorList(final Integer loanId) 
{
+        return (java.util.ArrayList<HashMap>) 
Utils.performServerGet(this.requestSpec, this.responseSpec,
+                LOAN_URL + loanId + GUARANTOR_API_URL + TENANT, "");
+    }
+
     // TODO: Rewrite to use fineract-client instead!
     // Example: 
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
     // org.apache.fineract.client.models.PostLoansLoanIdRequest)
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorTestBuilder.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorTestBuilder.java
index 0b793373dc..ef2e5707d6 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorTestBuilder.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/guarantor/GuarantorTestBuilder.java
@@ -31,6 +31,7 @@ public class GuarantorTestBuilder {
     @SuppressWarnings("unused")
     private static final String GUARANTOR_TYPE_STAFF = "2";
     private static final String GUARANTOR_TYPE_EXTERNAL = "3";
+    private static final String GUARANTOR_TYPE_GROUP = "4";
 
     private String guarantorTypeId = "1";
     private String entityId = null;
@@ -55,7 +56,7 @@ public class GuarantorTestBuilder {
             map.put("state", state);
             map.put("zip", zip);
 
-        } else if (GUARANTOR_TYPE_CUSTOMER.equals(guarantorTypeId)) {
+        } else if (GUARANTOR_TYPE_CUSTOMER.equals(guarantorTypeId) || 
GUARANTOR_TYPE_GROUP.equals(guarantorTypeId)) {
             map.put("entityId", entityId);
             map.put("amount", guaranteeAmount);
             map.put("savingsId", savingsId);
@@ -81,6 +82,15 @@ public class GuarantorTestBuilder {
         return this;
     }
 
+    public GuarantorTestBuilder existingGroupWithGuaranteeAmount(final String 
entityId, final String savingsId,
+            final String guaranteeAmount) {
+        this.entityId = entityId;
+        this.savingsId = savingsId;
+        this.guaranteeAmount = guaranteeAmount;
+        this.guarantorTypeId = GUARANTOR_TYPE_GROUP;
+        return this;
+    }
+
     public GuarantorTestBuilder externalCustomer() {
         this.guarantorTypeId = GUARANTOR_TYPE_EXTERNAL;
         return this;


Reply via email to