FINERACT-2498: Add GROUP as a Guarantor Type to Support Group Savings Accounts as Loan Collateral

This commit is contained in:
Ralph Hopman
2026-02-20 11:44:46 +00:00
committed by Adam Saghy
parent 55c51b140e
commit 76d108953b
11 changed files with 387 additions and 9 deletions
@@ -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());
}
}
@@ -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;
@@ -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);
@@ -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
@@ -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());
}
}
@@ -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);
}
@@ -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());
}
@@ -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());
}
}
@@ -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);
@@ -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)
@@ -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;