diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java index 03f96300e..26fbb4a42 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java @@ -295,6 +295,175 @@ public class LoanProductAdvanceExt { @Column(name = "cheque_block_lien_released") private Boolean chequeBlockLienReleased; + // ---- Loans A/C (EMI) wizard fields ------------------------------------- + @Column(name = "loan_security_mandatory") + private String securityMandatory; + + @Column(name = "loan_guarantors_required") + private Integer guarantorsRequired; + + @Column(name = "loan_co_applicant") + private String coApplicant; + + @Column(name = "loan_third_party_security") + private String thirdPartySecurity; + + @Column(name = "loan_legal_opinion_required") + private Boolean legalOpinionRequired; + + @Column(name = "loan_property_insurance_required") + private Boolean propertyInsuranceRequired; + + @Column(name = "loan_register_charge") + private Boolean registerCharge; + + @Column(name = "loan_release_security_on_closure") + private Boolean releaseSecurityOnClosure; + + @Column(name = "loan_allow_multiple_securities") + private Boolean allowMultipleSecurities; + + @Column(name = "loan_allow_third_party_deposit") + private Boolean allowThirdPartyDeposit; + + @Column(name = "loan_min_age") + private Integer minAge; + + @Column(name = "loan_max_age_at_maturity") + private Integer maxAgeAtMaturity; + + @Column(name = "loan_min_cibil") + private Integer minCibil; + + @Column(name = "loan_min_monthly_income") + private BigDecimal minMonthlyIncome; + + @Column(name = "loan_max_foir") + private BigDecimal maxFoir; + + @Column(name = "loan_max_loans_per_customer") + private Integer maxLoansPerCustomer; + + @Column(name = "loan_disbursement_type") + private String disbursementType; + + @Column(name = "loan_disbursement_mode") + private String disbursementMode; + + @Column(name = "loan_undisbursed_interest") + private String undisbursedInterest; + + @Column(name = "loan_pre_emi_interest") + private Boolean preEmiInterest; + + @Column(name = "loan_emi_due_day") + private String emiDueDay; + + @Column(name = "loan_holiday_rule") + private String holidayRule; + + @Column(name = "loan_moratorium_months") + private Integer moratoriumMonths; + + @Column(name = "loan_moratorium_interest") + private String moratoriumInterest; + + @Column(name = "loan_variable_installments") + private Boolean variableInstallments; + + @Column(name = "loan_repayment_mode") + private String repaymentMode; + + @Column(name = "loan_linked_account_type") + private String linkedAccountType; + + @Column(name = "loan_pdc_collection") + private String pdcCollection; + + @Column(name = "loan_mandate_required") + private Boolean mandateRequired; + + @Column(name = "loan_advance_emi") + private String advanceEmi; + + @Column(name = "loan_rate_type") + private String rateType; + + @Column(name = "loan_register_particular") + private String registerParticular; + + @Column(name = "loan_apply_cibil_adjustment") + private Boolean applyCibilAdjustment; + + @Column(name = "loan_rate_resolved_at") + private String rateResolvedAt; + + @Column(name = "loan_calc_period") + private String calcPeriod; + + @Column(name = "loan_interest_starts_from") + private String interestStartsFrom; + + @Column(name = "loan_compounding") + private String compounding; + + @Column(name = "loan_benchmark") + private String benchmark; + + @Column(name = "loan_reset_frequency") + private String resetFrequency; + + @Column(name = "loan_reset_change") + private String resetChange; + + @Column(name = "loan_part_prepayment_allowed") + private Boolean partPrepaymentAllowed; + + @Column(name = "loan_min_part_payment") + private BigDecimal minPartPayment; + + @Column(name = "loan_allowed_on") + private String allowedOn; + + @Column(name = "loan_part_payments_per_year") + private String partPaymentsPerYear; + + @Column(name = "loan_part_prepayment_mode") + private String partPrepaymentMode; + + @Column(name = "loan_foreclosure_floating_individual") + private BigDecimal foreclosureFloatingIndividual; + + @Column(name = "loan_foreclosure_fixed_individual") + private BigDecimal foreclosureFixedIndividual; + + @Column(name = "loan_foreclosure_non_individual") + private BigDecimal foreclosureNonIndividual; + + @Column(name = "loan_foreclosure_takeover") + private BigDecimal foreclosureTakeover; + + @Column(name = "loan_foreclosure_quote_valid_days") + private Integer foreclosureQuoteValidDays; + + @Column(name = "loan_auto_close_on_zero_balance") + private Boolean autoCloseOnZeroBalance; + + @Column(name = "loan_noc_letter") + private String nocLetter; + + @Column(name = "loan_restructuring") + private String restructuring; + + @Column(name = "loan_release_collateral_on_closure") + private Boolean releaseCollateralOnClosure; + + @Column(name = "loan_top_up_allowed") + private Boolean topUpAllowed; + + @Column(name = "loan_balance_transfer_out_allowed") + private Boolean balanceTransferOutAllowed; + public LoanProductAdvanceExt(final LoanProduct loanProduct) { this.loanProduct = loanProduct; this.loanProductId = loanProduct.getId(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index c4ec760a5..48158729a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -37,10 +37,15 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.UriInfo; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -58,6 +63,7 @@ import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.ApiFacingEnum; @@ -222,11 +228,54 @@ public class LoanProductsApiResource { return this.productMixDataApiJsonSerializer.serialize(settings, productMixes, PRODUCT_MIX_DATA_PARAMETERS); } - final Collection products = this.loanProductReadPlatformService.retrieveAllLoanProducts(); + final Collection products = enrichAndFilterProducts( + this.loanProductReadPlatformService.retrieveAllLoanProducts(), uriInfo); return this.toApiJsonSerializer.serialize(settings, products, LOAN_PRODUCT_DATA_PARAMETERS); } + /** + * Adds each product's category and type ("Advances A/C" or "Loans A/C" and its sub type) to the list, and applies + * the optional filters {@code activeOnly}, {@code productTypeGroup} and {@code productTypeSubGroup}. With none of + * them given the list holds the same products as before, each now carrying its classification. + */ + private Collection enrichAndFilterProducts(final Collection products, final UriInfo uriInfo) { + final MultivaluedMap query = uriInfo.getQueryParameters(); + final boolean activeOnly = Boolean.parseBoolean(query.getFirst("activeOnly")); + final String group = trimToNull(query.getFirst("productTypeGroup")); + final String subGroup = trimToNull(query.getFirst("productTypeSubGroup")); + final Map> extensions = this.loanProductAdvanceExtService.retrieveAllAsMap(); + final LocalDate today = DateUtils.getBusinessLocalDate(); + + final List result = new ArrayList<>(); + for (final LoanProductData product : products) { + final Map ext = extensions.getOrDefault(product.getId(), Collections.emptyMap()); + if (activeOnly && !isActiveOn(product, today)) { + continue; + } + if (group != null && !group.equalsIgnoreCase(String.valueOf(ext.get("productTypeGroup")))) { + continue; + } + if (subGroup != null && !subGroup.equalsIgnoreCase(String.valueOf(ext.get("productTypeSubGroup")))) { + continue; + } + product.setAdvanceExtensions(new LinkedHashMap<>(ext)); + result.add(product); + } + return result; + } + + /** A product is active from its start date until its close date; either end may be open. */ + private static boolean isActiveOn(final LoanProductData product, final LocalDate date) { + final LocalDate start = product.getStartDate(); + final LocalDate close = product.getCloseDate(); + return (start == null || !start.isAfter(date)) && (close == null || !close.isBefore(date)); + } + + private static String trimToNull(final String value) { + return value == null || value.isBlank() ? null : value.trim(); + } + @GET @Path("template") @Consumes({ MediaType.APPLICATION_JSON }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java index 9308653d1..bc233bc74 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java @@ -61,7 +61,22 @@ public class LoanProductAdvanceExtService { "chequeBookSizes", "chequeMaxBooksOutstanding", "chequeReissueWhenLeaves", "chequeFirstBookFree", "chequeFreeBooksPerYear", "chequeNamePrinted", "chequePrintDescription", "chequeStopPaymentAllowed", "chequeMaintainStockRegister", "chequeAllowWithdrawalSlip", "chequeBlockNpa", "chequeBlockLimitExpired", - "chequeBlockOverDp", "chequeBlockFrozen", "chequeBlockLienReleased" }; + "chequeBlockOverDp", "chequeBlockFrozen", "chequeBlockLienReleased", + // Loans A/C (EMI) wizard fields + "securityMandatory", "guarantorsRequired", "coApplicant", "thirdPartySecurity", + "legalOpinionRequired", "propertyInsuranceRequired", "registerCharge", "releaseSecurityOnClosure", + "allowMultipleSecurities", "allowThirdPartyDeposit", "minAge", "maxAgeAtMaturity", + "minCibil", "minMonthlyIncome", "maxFoir", "maxLoansPerCustomer", + "disbursementType", "disbursementMode", "undisbursedInterest", "preEmiInterest", + "emiDueDay", "holidayRule", "moratoriumMonths", "moratoriumInterest", + "variableInstallments", "repaymentMode", "linkedAccountType", "pdcCollection", + "mandateRequired", "advanceEmi", "rateType", "registerParticular", + "applyCibilAdjustment", "rateResolvedAt", "calcPeriod", "interestStartsFrom", + "compounding", "benchmark", "resetFrequency", "resetChange", + "partPrepaymentAllowed", "minPartPayment", "allowedOn", "partPaymentsPerYear", + "partPrepaymentMode", "foreclosureFloatingIndividual", "foreclosureFixedIndividual", "foreclosureNonIndividual", + "foreclosureTakeover", "foreclosureQuoteValidDays", "autoCloseOnZeroBalance", "nocLetter", + "restructuring", "releaseCollateralOnClosure", "topUpAllowed", "balanceTransferOutAllowed" }; /** * Insert or partial-update the satellite for this product. Returns the fields that actually changed so the @@ -84,6 +99,18 @@ public class LoanProductAdvanceExtService { .orElseGet(LinkedHashMap::new); } + /** + * Every product's satellite in one query, keyed by loan product id. Used by the product list so a screen can + * show and filter products by category and type without one extra call per product. + */ + public Map> retrieveAllAsMap() { + final Map> byProduct = new LinkedHashMap<>(); + for (final LoanProductAdvanceExt ext : this.repository.findAll()) { + byProduct.put(ext.getLoanProductId(), toMap(ext)); + } + return byProduct; + } + private boolean hasAnyAdvanceParameter(final JsonCommand command) { for (final String param : ADVANCE_PARAMETERS) { if (command.parameterExists(param)) { @@ -205,6 +232,63 @@ public class LoanProductAdvanceExtService { setBoolean(command, "chequeBlockFrozen", ext::getChequeBlockFrozen, ext::setChequeBlockFrozen, changes); setBoolean(command, "chequeBlockLienReleased", ext::getChequeBlockLienReleased, ext::setChequeBlockLienReleased, changes); + // Loans A/C (EMI) wizard fields + setString(command, "securityMandatory", ext::getSecurityMandatory, ext::setSecurityMandatory, changes); + setInteger(command, "guarantorsRequired", ext::getGuarantorsRequired, ext::setGuarantorsRequired, changes); + setString(command, "coApplicant", ext::getCoApplicant, ext::setCoApplicant, changes); + setString(command, "thirdPartySecurity", ext::getThirdPartySecurity, ext::setThirdPartySecurity, changes); + setBoolean(command, "legalOpinionRequired", ext::getLegalOpinionRequired, ext::setLegalOpinionRequired, changes); + setBoolean(command, "propertyInsuranceRequired", ext::getPropertyInsuranceRequired, ext::setPropertyInsuranceRequired, changes); + setBoolean(command, "registerCharge", ext::getRegisterCharge, ext::setRegisterCharge, changes); + setBoolean(command, "releaseSecurityOnClosure", ext::getReleaseSecurityOnClosure, ext::setReleaseSecurityOnClosure, changes); + setBoolean(command, "allowMultipleSecurities", ext::getAllowMultipleSecurities, ext::setAllowMultipleSecurities, changes); + setBoolean(command, "allowThirdPartyDeposit", ext::getAllowThirdPartyDeposit, ext::setAllowThirdPartyDeposit, changes); + setInteger(command, "minAge", ext::getMinAge, ext::setMinAge, changes); + setInteger(command, "maxAgeAtMaturity", ext::getMaxAgeAtMaturity, ext::setMaxAgeAtMaturity, changes); + setInteger(command, "minCibil", ext::getMinCibil, ext::setMinCibil, changes); + setDecimal(command, "minMonthlyIncome", ext::getMinMonthlyIncome, ext::setMinMonthlyIncome, changes); + setDecimal(command, "maxFoir", ext::getMaxFoir, ext::setMaxFoir, changes); + setInteger(command, "maxLoansPerCustomer", ext::getMaxLoansPerCustomer, ext::setMaxLoansPerCustomer, changes); + setString(command, "disbursementType", ext::getDisbursementType, ext::setDisbursementType, changes); + setString(command, "disbursementMode", ext::getDisbursementMode, ext::setDisbursementMode, changes); + setString(command, "undisbursedInterest", ext::getUndisbursedInterest, ext::setUndisbursedInterest, changes); + setBoolean(command, "preEmiInterest", ext::getPreEmiInterest, ext::setPreEmiInterest, changes); + setString(command, "emiDueDay", ext::getEmiDueDay, ext::setEmiDueDay, changes); + setString(command, "holidayRule", ext::getHolidayRule, ext::setHolidayRule, changes); + setInteger(command, "moratoriumMonths", ext::getMoratoriumMonths, ext::setMoratoriumMonths, changes); + setString(command, "moratoriumInterest", ext::getMoratoriumInterest, ext::setMoratoriumInterest, changes); + setBoolean(command, "variableInstallments", ext::getVariableInstallments, ext::setVariableInstallments, changes); + setString(command, "repaymentMode", ext::getRepaymentMode, ext::setRepaymentMode, changes); + setString(command, "linkedAccountType", ext::getLinkedAccountType, ext::setLinkedAccountType, changes); + setString(command, "pdcCollection", ext::getPdcCollection, ext::setPdcCollection, changes); + setBoolean(command, "mandateRequired", ext::getMandateRequired, ext::setMandateRequired, changes); + setString(command, "advanceEmi", ext::getAdvanceEmi, ext::setAdvanceEmi, changes); + setString(command, "rateType", ext::getRateType, ext::setRateType, changes); + setString(command, "registerParticular", ext::getRegisterParticular, ext::setRegisterParticular, changes); + setBoolean(command, "applyCibilAdjustment", ext::getApplyCibilAdjustment, ext::setApplyCibilAdjustment, changes); + setString(command, "rateResolvedAt", ext::getRateResolvedAt, ext::setRateResolvedAt, changes); + setString(command, "calcPeriod", ext::getCalcPeriod, ext::setCalcPeriod, changes); + setString(command, "interestStartsFrom", ext::getInterestStartsFrom, ext::setInterestStartsFrom, changes); + setString(command, "compounding", ext::getCompounding, ext::setCompounding, changes); + setString(command, "benchmark", ext::getBenchmark, ext::setBenchmark, changes); + setString(command, "resetFrequency", ext::getResetFrequency, ext::setResetFrequency, changes); + setString(command, "resetChange", ext::getResetChange, ext::setResetChange, changes); + setBoolean(command, "partPrepaymentAllowed", ext::getPartPrepaymentAllowed, ext::setPartPrepaymentAllowed, changes); + setDecimal(command, "minPartPayment", ext::getMinPartPayment, ext::setMinPartPayment, changes); + setString(command, "allowedOn", ext::getAllowedOn, ext::setAllowedOn, changes); + setString(command, "partPaymentsPerYear", ext::getPartPaymentsPerYear, ext::setPartPaymentsPerYear, changes); + setString(command, "partPrepaymentMode", ext::getPartPrepaymentMode, ext::setPartPrepaymentMode, changes); + setDecimal(command, "foreclosureFloatingIndividual", ext::getForeclosureFloatingIndividual, ext::setForeclosureFloatingIndividual, changes); + setDecimal(command, "foreclosureFixedIndividual", ext::getForeclosureFixedIndividual, ext::setForeclosureFixedIndividual, changes); + setDecimal(command, "foreclosureNonIndividual", ext::getForeclosureNonIndividual, ext::setForeclosureNonIndividual, changes); + setDecimal(command, "foreclosureTakeover", ext::getForeclosureTakeover, ext::setForeclosureTakeover, changes); + setInteger(command, "foreclosureQuoteValidDays", ext::getForeclosureQuoteValidDays, ext::setForeclosureQuoteValidDays, changes); + setBoolean(command, "autoCloseOnZeroBalance", ext::getAutoCloseOnZeroBalance, ext::setAutoCloseOnZeroBalance, changes); + setString(command, "nocLetter", ext::getNocLetter, ext::setNocLetter, changes); + setString(command, "restructuring", ext::getRestructuring, ext::setRestructuring, changes); + setBoolean(command, "releaseCollateralOnClosure", ext::getReleaseCollateralOnClosure, ext::setReleaseCollateralOnClosure, changes); + setBoolean(command, "topUpAllowed", ext::getTopUpAllowed, ext::setTopUpAllowed, changes); + setBoolean(command, "balanceTransferOutAllowed", ext::getBalanceTransferOutAllowed, ext::setBalanceTransferOutAllowed, changes); } // ---- typed setters: only write params that are present, and only report real changes ---------- @@ -368,6 +452,63 @@ public class LoanProductAdvanceExtService { map.put("chequeBlockOverDp", ext.getChequeBlockOverDp()); map.put("chequeBlockFrozen", ext.getChequeBlockFrozen()); map.put("chequeBlockLienReleased", ext.getChequeBlockLienReleased()); + // Loans A/C (EMI) wizard fields + map.put("securityMandatory", ext.getSecurityMandatory()); + map.put("guarantorsRequired", ext.getGuarantorsRequired()); + map.put("coApplicant", ext.getCoApplicant()); + map.put("thirdPartySecurity", ext.getThirdPartySecurity()); + map.put("legalOpinionRequired", ext.getLegalOpinionRequired()); + map.put("propertyInsuranceRequired", ext.getPropertyInsuranceRequired()); + map.put("registerCharge", ext.getRegisterCharge()); + map.put("releaseSecurityOnClosure", ext.getReleaseSecurityOnClosure()); + map.put("allowMultipleSecurities", ext.getAllowMultipleSecurities()); + map.put("allowThirdPartyDeposit", ext.getAllowThirdPartyDeposit()); + map.put("minAge", ext.getMinAge()); + map.put("maxAgeAtMaturity", ext.getMaxAgeAtMaturity()); + map.put("minCibil", ext.getMinCibil()); + map.put("minMonthlyIncome", ext.getMinMonthlyIncome()); + map.put("maxFoir", ext.getMaxFoir()); + map.put("maxLoansPerCustomer", ext.getMaxLoansPerCustomer()); + map.put("disbursementType", ext.getDisbursementType()); + map.put("disbursementMode", ext.getDisbursementMode()); + map.put("undisbursedInterest", ext.getUndisbursedInterest()); + map.put("preEmiInterest", ext.getPreEmiInterest()); + map.put("emiDueDay", ext.getEmiDueDay()); + map.put("holidayRule", ext.getHolidayRule()); + map.put("moratoriumMonths", ext.getMoratoriumMonths()); + map.put("moratoriumInterest", ext.getMoratoriumInterest()); + map.put("variableInstallments", ext.getVariableInstallments()); + map.put("repaymentMode", ext.getRepaymentMode()); + map.put("linkedAccountType", ext.getLinkedAccountType()); + map.put("pdcCollection", ext.getPdcCollection()); + map.put("mandateRequired", ext.getMandateRequired()); + map.put("advanceEmi", ext.getAdvanceEmi()); + map.put("rateType", ext.getRateType()); + map.put("registerParticular", ext.getRegisterParticular()); + map.put("applyCibilAdjustment", ext.getApplyCibilAdjustment()); + map.put("rateResolvedAt", ext.getRateResolvedAt()); + map.put("calcPeriod", ext.getCalcPeriod()); + map.put("interestStartsFrom", ext.getInterestStartsFrom()); + map.put("compounding", ext.getCompounding()); + map.put("benchmark", ext.getBenchmark()); + map.put("resetFrequency", ext.getResetFrequency()); + map.put("resetChange", ext.getResetChange()); + map.put("partPrepaymentAllowed", ext.getPartPrepaymentAllowed()); + map.put("minPartPayment", ext.getMinPartPayment()); + map.put("allowedOn", ext.getAllowedOn()); + map.put("partPaymentsPerYear", ext.getPartPaymentsPerYear()); + map.put("partPrepaymentMode", ext.getPartPrepaymentMode()); + map.put("foreclosureFloatingIndividual", ext.getForeclosureFloatingIndividual()); + map.put("foreclosureFixedIndividual", ext.getForeclosureFixedIndividual()); + map.put("foreclosureNonIndividual", ext.getForeclosureNonIndividual()); + map.put("foreclosureTakeover", ext.getForeclosureTakeover()); + map.put("foreclosureQuoteValidDays", ext.getForeclosureQuoteValidDays()); + map.put("autoCloseOnZeroBalance", ext.getAutoCloseOnZeroBalance()); + map.put("nocLetter", ext.getNocLetter()); + map.put("restructuring", ext.getRestructuring()); + map.put("releaseCollateralOnClosure", ext.getReleaseCollateralOnClosure()); + map.put("topUpAllowed", ext.getTopUpAllowed()); + map.put("balanceTransferOutAllowed", ext.getBalanceTransferOutAllowed()); return map; } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index e05536f6c..d076887b3 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -622,4 +622,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_add_loan_product_loans_ext_fields.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_add_loan_product_loans_ext_fields.xml new file mode 100644 index 000000000..a05a790f6 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0597_add_loan_product_loans_ext_fields.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +