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;