feat(guarantor): enrich guarantor accounts API with loan health and borrower details

- Add borrower name, savings account, guaranteed/released/remaining amounts, loan timeline, and overdue info to GuarantorAccountSummaryData
- Fix duplicate rows in GuarantorLoanAccountSummaryDataMapper by joining savings account via guarantor funding details instead of client ID
- Fix NPE in GuarantorDomainServiceImpl when loan product guarantee details are null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-24 13:15:25 +05:30
co-authored by Claude Sonnet 4.6
parent 1eba2c50ed
commit fc80869d4b
7 changed files with 2537 additions and 2446 deletions
@@ -21,6 +21,12 @@ package org.apache.fineract.infrastructure.core.serialization;
import com.google.gson.ExclusionStrategy; import com.google.gson.ExclusionStrategy;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
@@ -112,5 +118,33 @@ public final class GoogleGsonSerializerHelper {
builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()); builder.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter());
builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter()); builder.registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeAdapter());
builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter()); builder.registerTypeAdapter(ExternalId.class, new ExternalIdAdapter());
// Use runtime type when serializing interface-typed fields (e.g. Collection<? extends IGuarantor>)
builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
if (!type.getRawType().isInterface()) {
return null;
}
final TypeAdapterFactory self = this;
return new TypeAdapter<T>() {
@Override
public void write(final JsonWriter out, final T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
TypeAdapter<T> delegate = (TypeAdapter<T>) gson.getDelegateAdapter(self,
TypeToken.get((Class<T>) value.getClass()));
delegate.write(out, value);
}
@Override
public T read(final JsonReader in) throws IOException {
return null;
}
};
}
});
} }
} }
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.accountdetails.data; package org.apache.fineract.portfolio.accountdetails.data;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
@@ -44,12 +45,26 @@ public class GuarantorAccountSummaryData {
private final Boolean isActive; private final Boolean isActive;
private final String relationship; private final String relationship;
private final BigDecimal onHoldAmount; private final BigDecimal onHoldAmount;
private final String savingsAccountNo;
private final BigDecimal amountReleased;
private final String borrowerName;
private final BigDecimal totalOverdue;
private final LocalDate overdueSinceDate;
private final LocalDate submittedOnDate;
private final LocalDate approvedOnDate;
private final LocalDate disbursedOnDate;
private final LocalDate expectedMaturityDate;
private final BigDecimal amountRemaining;
private final BigDecimal savingsBalance;
public GuarantorAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId, public GuarantorAccountSummaryData(final Long id, final String accountNo, final String externalId, final Long productId,
final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus, final String loanProductName, final String shortLoanProductName, final LoanStatusEnumData loanStatus,
final EnumOptionData loanType, final Integer loanCycle, final Boolean inArrears, final BigDecimal originalLoan, final EnumOptionData loanType, final Integer loanCycle, final Boolean inArrears, final BigDecimal originalLoan,
final BigDecimal loanBalance, final BigDecimal amountPaid, final Boolean isActive, final String relationship, final BigDecimal loanBalance, final BigDecimal amountPaid, final Boolean isActive, final String relationship,
final BigDecimal onHoldAmount) { final BigDecimal onHoldAmount, final String savingsAccountNo, final BigDecimal amountReleased,
final String borrowerName, final BigDecimal totalOverdue, final LocalDate overdueSinceDate,
final LocalDate submittedOnDate, final LocalDate approvedOnDate, final LocalDate disbursedOnDate,
final LocalDate expectedMaturityDate, final BigDecimal amountRemaining, final BigDecimal savingsBalance) {
this.id = id; this.id = id;
this.accountNo = accountNo; this.accountNo = accountNo;
this.externalId = externalId; this.externalId = externalId;
@@ -66,5 +81,16 @@ public class GuarantorAccountSummaryData {
this.isActive = isActive; this.isActive = isActive;
this.relationship = relationship; this.relationship = relationship;
this.onHoldAmount = onHoldAmount; this.onHoldAmount = onHoldAmount;
this.savingsAccountNo = savingsAccountNo;
this.amountReleased = amountReleased;
this.borrowerName = borrowerName;
this.totalOverdue = totalOverdue;
this.overdueSinceDate = overdueSinceDate;
this.submittedOnDate = submittedOnDate;
this.approvedOnDate = approvedOnDate;
this.disbursedOnDate = disbursedOnDate;
this.expectedMaturityDate = expectedMaturityDate;
this.amountRemaining = amountRemaining;
this.savingsBalance = savingsBalance;
} }
} }
@@ -725,7 +725,8 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
accountsSummary.append(" l.product_id as productId, lp.name as productName, lp.short_name as shortProductName,") accountsSummary.append(" l.product_id as productId, lp.name as productName, lp.short_name as shortProductName,")
.append(" l.loan_status_id as statusId, l.loan_type_enum as loanType,") .append(" l.loan_status_id as statusId, l.loan_type_enum as loanType,")
.append("l.principal_disbursed_derived as originalLoan,").append("l.total_outstanding_derived as loanBalance,") .append(" COALESCE(NULLIF(l.principal_disbursed_derived, 0), l.principal_amount) as originalLoan,")
.append("l.total_outstanding_derived as loanBalance,")
.append("l.total_repayment_derived as amountPaid,") .append("l.total_repayment_derived as amountPaid,")
.append(" l.loan_product_counter as loanCycle,") .append(" l.loan_product_counter as loanCycle,")
@@ -737,12 +738,23 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
.append(" l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate,") .append(" l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate,")
.append(" l.closedon_date as closedOnDate,").append(" la.overdue_since_date_derived as overdueSinceDate,") .append(" l.closedon_date as closedOnDate,").append(" la.overdue_since_date_derived as overdueSinceDate,")
.append(" l.writtenoffon_date as writtenOffOnDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate,") .append(" l.writtenoffon_date as writtenOffOnDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate,")
.append(" g.is_active as isActive,").append(" cv.code_value as relationship,").append(" sa.on_hold_funds_derived") .append(" g.is_active as isActive,").append(" cv.code_value as relationship,")
.append(" gfd.amount as on_hold_funds_derived,")
.append(" sa.account_no as savingsAccountNo,")
.append(" sa.account_balance_derived as savingsBalance,")
.append(" gfd.amount_released_derived as amountReleased,")
.append(" gfd.amount_remaining_derived as amountRemaining,")
.append(" bc.display_name as borrowerName,")
.append(" COALESCE(la.total_overdue_derived, 0) as totalOverdue")
.append(" from m_loan l ").append(" join m_guarantor as g on g.loan_id = l.id ") .append(" from m_loan l ").append(" join m_guarantor as g on g.loan_id = l.id ")
.append(" join m_client as c on c.id = g.entity_id ").append(" LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id") .append(" join m_client as c on c.id = g.entity_id ")
.append(" join m_client as bc on bc.id = l.client_id ")
.append(" LEFT JOIN m_product_loan AS lp ON lp.id = l.product_id")
.append(" left join m_loan_arrears_aging la on la.loan_id = l.id") .append(" left join m_loan_arrears_aging la on la.loan_id = l.id")
.append(" left join m_code_value cv ON cv.id = g.client_reln_cv_id") .append(" left join m_code_value cv ON cv.id = g.client_reln_cv_id")
.append(" left join m_savings_account sa on sa.client_id = c.id"); .append(" left join m_guarantor_funding_details gfd on gfd.guarantor_id = g.id")
.append(" left join m_portfolio_account_associations paa on paa.id = gfd.account_associations_id")
.append(" left join m_savings_account sa on sa.id = paa.linked_savings_account_id");
return accountsSummary.toString(); return accountsSummary.toString();
} }
@@ -776,8 +788,20 @@ public class AccountDetailsReadPlatformServiceJpaRepositoryImpl implements Accou
final Boolean isActive = rs.getBoolean("isActive"); final Boolean isActive = rs.getBoolean("isActive");
final String relationship = rs.getString("relationship"); final String relationship = rs.getString("relationship");
final String savingsAccountNo = rs.getString("savingsAccountNo");
final BigDecimal savingsBalance = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "savingsBalance");
final BigDecimal amountReleased = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "amountReleased");
final BigDecimal amountRemaining = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "amountRemaining");
final String borrowerName = rs.getString("borrowerName");
final BigDecimal totalOverdue = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "totalOverdue");
final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate");
final LocalDate approvedOnDate = JdbcSupport.getLocalDate(rs, "approvedOnDate");
final LocalDate disbursedOnDate = JdbcSupport.getLocalDate(rs, "actualDisbursementDate");
final LocalDate expectedMaturityDate = JdbcSupport.getLocalDate(rs, "expectedMaturityDate");
return new GuarantorAccountSummaryData(id, accountNo, externalId, productId, loanProductName, shortLoanProductName, loanStatus, return new GuarantorAccountSummaryData(id, accountNo, externalId, productId, loanProductName, shortLoanProductName, loanStatus,
loanType, loanCycle, inArrears, originalLoan, loanBalance, amountPaid, isActive, relationship, onHoldAmount); loanType, loanCycle, inArrears, originalLoan, loanBalance, amountPaid, isActive, relationship, onHoldAmount,
savingsAccountNo, amountReleased, borrowerName, totalOverdue, overdueSinceDate,
submittedOnDate, approvedOnDate, disbursedOnDate, expectedMaturityDate, amountRemaining, savingsBalance);
} }
} }
@@ -725,7 +725,10 @@ public class ClientReadPlatformServiceImpl implements ClientReadPlatformService
builder.append("c.alternate_dip_verified as alternateDipVerified, "); builder.append("c.alternate_dip_verified as alternateDipVerified, ");
builder.append("c.alternate_dip_lookup_log_id as alternateDipLookupLogId, "); builder.append("c.alternate_dip_lookup_log_id as alternateDipLookupLogId, ");
builder.append("c.alternate_dip_timestamp as alternateDipTimestamp, "); builder.append("c.alternate_dip_timestamp as alternateDipTimestamp, ");
builder.append("c.dip_lookup_log_id as dipLookupLogId, ");
builder.append("c.dip_timestamp as dipTimestamp, ");
builder.append("c.name_verified as nameVerified, "); builder.append("c.name_verified as nameVerified, ");
builder.append("c.screening_log_id as screeningLogId, ");
builder.append("c.submittedon_date as submittedOnDate, "); builder.append("c.submittedon_date as submittedOnDate, ");
builder.append("sbu.username as submittedByUsername, "); builder.append("sbu.username as submittedByUsername, ");
@@ -150,7 +150,8 @@ public class GuarantorData implements IGuarantor {
public static GuarantorData mergeClientData(final ClientData clientData, final GuarantorData guarantorData) { public static GuarantorData mergeClientData(final ClientData clientData, final GuarantorData guarantorData) {
return new GuarantorData(guarantorData.id, guarantorData.loanId, guarantorData.clientRelationshipType, guarantorData.entityId, return new GuarantorData(guarantorData.id, guarantorData.loanId, guarantorData.clientRelationshipType, guarantorData.entityId,
guarantorData.guarantorType, clientData.getFirstname(), clientData.getLastname(), null, null, null, null, null, null, null, guarantorData.guarantorType, clientData.getFirstname(), clientData.getLastname(), null, null, null, null, null, null, null,
null, null, null, clientData.getOfficeName(), clientData.getActivationDate(), clientData.getExternalId().getValue(), null, null, null, clientData.getOfficeName(), clientData.getActivationDate(),
clientData.getExternalId() != null ? clientData.getExternalId().getValue() : null,
guarantorData.status, guarantorData.guarantorFundingDetails, null, guarantorData.allowedClientRelationshipTypes, guarantorData.status, guarantorData.guarantorFundingDetails, null, guarantorData.allowedClientRelationshipTypes,
guarantorData.accountLinkingOptions); guarantorData.accountLinkingOptions);
} }
@@ -111,6 +111,9 @@ public class GuarantorDomainServiceImpl implements GuarantorDomainService {
BigDecimal principal = loan.getPrincipal().getAmount(); BigDecimal principal = loan.getPrincipal().getAmount();
if (loanProduct.isHoldGuaranteeFunds()) { if (loanProduct.isHoldGuaranteeFunds()) {
LoanProductGuaranteeDetails guaranteeData = loanProduct.getLoanProductGuaranteeDetails(); LoanProductGuaranteeDetails guaranteeData = loanProduct.getLoanProductGuaranteeDetails();
if (guaranteeData == null) {
return;
}
final List<Guarantor> existGuarantorList = this.guarantorRepository.findByLoan(loan); final List<Guarantor> existGuarantorList = this.guarantorRepository.findByLoan(loan);
BigDecimal mandatoryAmount = principal.multiply(guaranteeData.getMandatoryGuarantee()).divide(BigDecimal.valueOf(100)); BigDecimal mandatoryAmount = principal.multiply(guaranteeData.getMandatoryGuarantee()).divide(BigDecimal.valueOf(100));
BigDecimal minSelfAmount = principal.multiply(guaranteeData.getMinimumGuaranteeFromOwnFunds()).divide(BigDecimal.valueOf(100)); BigDecimal minSelfAmount = principal.multiply(guaranteeData.getMinimumGuaranteeFromOwnFunds()).divide(BigDecimal.valueOf(100));