From 22c4403a14668a8aff664cdaa63ff8fea26c87b3 Mon Sep 17 00:00:00 2001 From: Sonali Likhitkar Date: Tue, 29 Sep 2026 17:56:50 +0530 Subject: [PATCH] feat(loan-product): add advance loan product ext entity, API and liquibase --- .../loanproduct/data/LoanProductData.java | 14 + .../domain/LoanProductAdvanceExt.java | 302 ++++++++++++++ .../LoanProductAdvanceExtRepository.java | 16 + .../api/LoanProductsApiResource.java | 8 +- .../LoanProductDataValidator.java | 8 + .../service/LoanProductAdvanceExtService.java | 373 ++++++++++++++++++ ...WritePlatformServiceJpaRepositoryImpl.java | 11 + .../starter/LoanProductConfiguration.java | 6 +- .../db/changelog/tenant/changelog-tenant.xml | 2 + .../0571_create_loan_product_advance_ext.xml | 112 ++++++ ...72_add_loan_product_advance_ext_fields.xml | 71 ++++ 11 files changed, 920 insertions(+), 3 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExtRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_create_loan_product_advance_ext.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_loan_product_advance_ext_fields.xml diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index 94f0f5bea..a68c170bb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -225,6 +225,12 @@ public class LoanProductData implements Serializable { private final LoanProductGuaranteeData productGuaranteeData; private final Boolean accountMovesOutOfNPAOnlyOnArrearsCompletion; private LoanProductConfigurableAttributes allowAttributeOverrides; + + /** + * "Advances A/C" satellite fields (security, lien, renewal, npa, cheque book), flat by their form field + * name. Set after construction (like {@link #allowAttributeOverrides}) so the edit form can repopulate. + */ + private Map advanceExtensions; private final boolean syncExpectedWithDisbursementDate; private final boolean isEqualAmortization; private final BigDecimal fixedPrincipalPercentagePerInstallment; @@ -1466,6 +1472,14 @@ public class LoanProductData implements Serializable { this.allowAttributeOverrides = loanProductConfigurableAttributes; } + public void setAdvanceExtensions(final Map advanceExtensions) { + this.advanceExtensions = advanceExtensions; + } + + public Map getAdvanceExtensions() { + return this.advanceExtensions; + } + public boolean isIsLinkedToFloatingInterestRates() { return this.isLinkedToFloatingInterestRates; } 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 new file mode 100644 index 000000000..03f96300e --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExt.java @@ -0,0 +1,302 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.apache.fineract.portfolio.loanproduct.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * "Advances A/C" product configuration that Fineract's loan-product schema has no home for + * (security & margin, lien, deposit-linked interest, penal/NPA handling, renewal, monitoring, + * cheque book). It is a standalone satellite keyed by {@code loan_product_id} rather than a widening + * of {@code m_product_loan} or a new relation on {@link LoanProduct}: the product entity graph is + * loaded on the hot loan-origination path, so it is deliberately left untouched. + * + * Typed columns (not a JSON blob) so reports can filter on them and GL ids get a real FK. + */ +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "m_product_loan_advance_ext") +public class LoanProductAdvanceExt { + + @Id + @Column(name = "loan_product_id") + private Long loanProductId; + + /** Shared primary key, mapped read-only; {@link #loanProductId} is the writable side. */ + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "loan_product_id", insertable = false, updatable = false) + private LoanProduct loanProduct; + + // ---- classification ------------------------------------------------- + @Column(name = "product_type_group") + private String productTypeGroup; + + @Column(name = "product_type_sub_group") + private String productTypeSubGroup; + + @Column(name = "gl_account_id") + private Long glAccountId; + + @Column(name = "loan_product_category") + private String loanProductCategory; + + @Column(name = "priority_sector") + private String prioritySector; + + @Column(name = "cibil_account_type") + private String cibilAccountType; + + @Column(name = "kfs_enabled") + private Boolean kfsEnabled; + + @Column(name = "apr_enabled") + private Boolean aprEnabled; + + // ---- facility ------------------------------------------------------- + @Column(name = "facility_type") + private String facilityType; + + // ---- security & margin ---------------------------------------------- + @Column(name = "security_type") + private String securityType; + + @Column(name = "charge_type") + private String chargeType; + + @Column(name = "eligible_amount_basis") + private String eligibleAmountBasis; + + @Column(name = "margin_pct") + private BigDecimal marginPct; + + @Column(name = "ltv_pct") + private BigDecimal ltvPct; + + @Column(name = "dp_method") + private String dpMethod; + + // ---- lien ----------------------------------------------------------- + @Column(name = "lien_required") + private Boolean lienRequired; + + @Column(name = "lien_marking_stage") + private String lienMarkingStage; + + @Column(name = "lien_certificate_required") + private Boolean lienCertificateRequired; + + @Column(name = "lien_certificate_prefix") + private String lienCertificatePrefix; + + @Column(name = "print_certificate_on_disbursement") + private Boolean printCertificateOnDisbursement; + + // ---- interest (extras; rate bounds live on the core product) -------- + @Column(name = "interest_basis") + private String interestBasis; + + @Column(name = "spread_pct") + private BigDecimal spreadPct; + + @Column(name = "interest_charged_on") + private String interestChargedOn; + + @Column(name = "interest_posting_frequency") + private String interestPostingFrequency; + + // ---- penal & NPA (extras; npaDays/grace/tolerance live on core) ------ + @Column(name = "penal_applicable") + private Boolean penalApplicable; + + @Column(name = "penal_rate_pct") + private BigDecimal penalRatePct; + + @Column(name = "penal_applied_on") + private String penalAppliedOn; + + @Column(name = "penal_grace_days") + private Integer penalGraceDays; + + @Column(name = "npa_trigger") + private String npaTrigger; + + @Column(name = "interest_after_npa") + private String interestAfterNpa; + + @Column(name = "undebited_interest_gl_id") + private Long undebitedInterestGlId; + + // ---- renewal & closure ---------------------------------------------- + @Column(name = "renewal_allowed") + private Boolean renewalAllowed; + + @Column(name = "renewal_period_months") + private Integer renewalPeriodMonths; + + @Column(name = "renewal_grace_days") + private Integer renewalGraceDays; + + @Column(name = "preclosure_allowed") + private String preclosureAllowed; + + @Column(name = "lock_in_months") + private Integer lockInMonths; + + @Column(name = "interest_on_preclosure") + private String interestOnPreclosure; + + // ---- monitoring ----------------------------------------------------- + @Column(name = "revaluation_freq_months") + private Integer revaluationFreqMonths; + + @Column(name = "insurance_required") + private Boolean insuranceRequired; + + @Column(name = "stock_statement_freq_months") + private Integer stockStatementFreqMonths; + + // ---- cheque book ---------------------------------------------------- + @Column(name = "cheque_book_enabled") + private Boolean chequeBookEnabled; + + @Column(name = "cheque_type") + private String chequeType; + + @Column(name = "default_cheque_book_size") + private Integer defaultChequeBookSize; + + @Column(name = "pps_threshold") + private BigDecimal ppsThreshold; + + @Column(name = "pps_mandatory_above") + private BigDecimal ppsMandatoryAbove; + + // ---- share (co-op) -------------------------------------------------- + @Column(name = "share_capital_gl_id") + private Long shareCapitalGlId; + + // ---- security & lien extras ----------------------------------------- + /** Pledgeable deposit product names, joined with "|". */ + @Column(name = "eligible_deposit_products") + private String eligibleDepositProducts; + + @Column(name = "lien_tenure_not_exceed_maturity") + private Boolean lienTenureNotExceedMaturity; + + @Column(name = "lien_auto_release_on_closure") + private Boolean lienAutoReleaseOnClosure; + + @Column(name = "lien_allow_multiple_deposits") + private Boolean lienAllowMultipleDeposits; + + @Column(name = "lien_adjust_from_maturity") + private Boolean lienAdjustFromMaturity; + + @Column(name = "lien_allow_third_party") + private Boolean lienAllowThirdParty; + + @Column(name = "lien_allow_renewal_under_lien") + private Boolean lienAllowRenewalUnderLien; + + // ---- limit & tenure extras ------------------------------------------ + @Column(name = "repayment_type") + private String repaymentType; + + @Column(name = "appropriation_order") + private String appropriationOrder; + + @Column(name = "debit_balance_allow") + private Boolean debitBalanceAllow; + + @Column(name = "allow_debit_after_due_date") + private Boolean allowDebitAfterDueDate; + + @Column(name = "branch_level_account_type") + private Boolean branchLevelAccountType; + + // ---- interest extras ------------------------------------------------ + @Column(name = "interest_calc_method") + private String interestCalcMethod; + + @Column(name = "interest_on_credit_balance") + private Boolean interestOnCreditBalance; + + @Column(name = "interest_posting_day") + private String interestPostingDay; + + // ---- NPA upgrade rules ---------------------------------------------- + @Column(name = "npa_upgrade_only_all_arrears") + private Boolean npaUpgradeOnlyAllArrears; + + @Column(name = "npa_keep_standard_if_deposit_covers") + private Boolean npaKeepStandardIfDepositCovers; + + // ---- cheque book full config ---------------------------------------- + /** Allowed book sizes (leaves), joined with "|". */ + @Column(name = "cheque_book_sizes") + private String chequeBookSizes; + + @Column(name = "cheque_max_books_outstanding") + private Integer chequeMaxBooksOutstanding; + + @Column(name = "cheque_reissue_when_leaves") + private Integer chequeReissueWhenLeaves; + + @Column(name = "cheque_first_book_free") + private Boolean chequeFirstBookFree; + + @Column(name = "cheque_free_books_per_year") + private Integer chequeFreeBooksPerYear; + + @Column(name = "cheque_name_printed") + private String chequeNamePrinted; + + @Column(name = "cheque_print_description") + private String chequePrintDescription; + + @Column(name = "cheque_stop_payment_allowed") + private Boolean chequeStopPaymentAllowed; + + @Column(name = "cheque_maintain_stock_register") + private Boolean chequeMaintainStockRegister; + + @Column(name = "cheque_allow_withdrawal_slip") + private Boolean chequeAllowWithdrawalSlip; + + @Column(name = "cheque_block_npa") + private Boolean chequeBlockNpa; + + @Column(name = "cheque_block_limit_expired") + private Boolean chequeBlockLimitExpired; + + @Column(name = "cheque_block_over_dp") + private Boolean chequeBlockOverDp; + + @Column(name = "cheque_block_frozen") + private Boolean chequeBlockFrozen; + + @Column(name = "cheque_block_lien_released") + private Boolean chequeBlockLienReleased; + + public LoanProductAdvanceExt(final LoanProduct loanProduct) { + this.loanProduct = loanProduct; + this.loanProductId = loanProduct.getId(); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExtRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExtRepository.java new file mode 100644 index 000000000..70ba4ccfb --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductAdvanceExtRepository.java @@ -0,0 +1,16 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.apache.fineract.portfolio.loanproduct.domain; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LoanProductAdvanceExtRepository extends JpaRepository { + + Optional findByLoanProductId(Long loanProductId); +} 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 bd3f87c43..c4ec760a5 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 @@ -107,6 +107,7 @@ import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundEx import org.apache.fineract.portfolio.loanproduct.productmix.data.ProductMixData; import org.apache.fineract.portfolio.loanproduct.productmix.service.ProductMixReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService; +import org.apache.fineract.portfolio.loanproduct.service.LoanProductAdvanceExtService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData; import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService; @@ -142,7 +143,8 @@ public class LoanProductsApiResource { LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanProductConstants.CAPITALIZED_INCOME_TYPE_PARAM_NAME, LoanProductConstants.ENABLE_BUY_DOWN_FEE_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_CALCULATION_TYPE_PARAM_NAME, - LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME)); + LoanProductConstants.BUY_DOWN_FEE_STRATEGY_PARAM_NAME, LoanProductConstants.BUY_DOWN_FEE_INCOME_TYPE_PARAM_NAME, + "advanceExtensions")); private static final Set PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>( Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions")); @@ -153,6 +155,7 @@ public class LoanProductsApiResource { private final PlatformSecurityContext context; private final LoanProductReadPlatformService loanProductReadPlatformService; + private final LoanProductAdvanceExtService loanProductAdvanceExtService; private final ChargeReadPlatformService chargeReadPlatformService; private final CurrencyReadPlatformService currencyReadPlatformService; private final FundReadPlatformService fundReadPlatformService; @@ -384,6 +387,9 @@ public class LoanProductsApiResource { if (settings.isTemplate()) { loanProduct = handleTemplate(loanProduct); } + // "Advances A/C" satellite fields, so the edit form can repopulate. Set last: the accounting and template + // steps above return fresh LoanProductData copies that would otherwise drop it. Empty map for a standard product. + loanProduct.setAdvanceExtensions(this.loanProductAdvanceExtService.retrieveAsMap(productId)); return this.toApiJsonSerializer.serialize(settings, loanProduct, LOAN_PRODUCT_DATA_PARAMETERS); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 0f06e0343..57a683d1d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -72,6 +72,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCom import org.apache.fineract.portfolio.loanproduct.domain.LoanPreCloseInterestCalculationStrategy; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.service.LoanProductAdvanceExtService; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType; import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; @@ -212,6 +213,13 @@ public final class LoanProductDataValidator { LoanProductConstants.ALLOW_FULL_TERM_FOR_TRANCHE_PARAM_NAME // )); + static { + // "Advances A/C" satellite params are persisted by LoanProductAdvanceExtService, which owns the + // authoritative list of their names — added here rather than restated so the two cannot drift, and so + // the unsupported-parameter check keeps rejecting genuine typos. + SUPPORTED_PARAMETERS.addAll(Arrays.asList(LoanProductAdvanceExtService.ADVANCE_PARAMETERS)); + } + private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, LoanProductConstants.interestCalculationPeriodTypeParamName, LoanProductConstants.inArrearsToleranceParamName, 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 new file mode 100644 index 000000000..9308653d1 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java @@ -0,0 +1,373 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.apache.fineract.portfolio.loanproduct.service; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExt; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExtRepository; +import org.springframework.stereotype.Service; + +/** + * Persists and reads the "Advances A/C" satellite ({@link LoanProductAdvanceExt}). + * + * Only touched for an Advances product: a standard loan product carries none of {@link #ADVANCE_PARAMETERS}, + * so {@link #saveOrUpdate} early-returns and never writes an empty row. Called from inside the loan-product + * create/update transaction, so a failure here rolls the whole product back. + */ +@Service +@RequiredArgsConstructor +public class LoanProductAdvanceExtService { + + private final LoanProductAdvanceExtRepository repository; + + /** + * Every Advance parameter name. Shared with {@code LoanProductDataValidator} (added to its + * SUPPORTED_PARAMETERS) so the two can never disagree about what is accepted. These are the + * satellite-only fields; core fields (principal, rate, tenure, npaDays, delinquencyBucketId, + * graceOnArrearsAgeing, inArrearsTolerance, accountingRule...) are handled by the standard product. + */ + public static final String[] ADVANCE_PARAMETERS = { "productTypeGroup", "productTypeSubGroup", "glAccount", + "loanProductCategory", "prioritySector", "cibilAccountType", "kfsEnabled", "aprEnabled", "facilityType", + "securityType", "chargeType", "eligibleAmountBasis", "margin", "ltv", "dpMethod", "lienRequired", + "lienMarkingStage", "lienCertificateRequired", "lienCertificatePrefix", "printCertificateOnDisbursement", + "interestBasis", "spread", "interestChargedOn", "interestPostingFrequency", "penalApplicable", "penalRate", + "penalAppliedOn", "penalGraceDays", "npaTrigger", "interestAfterNpa", "undebitedInterestGlId", + "renewalAllowed", "renewalPeriodMonths", "renewalGraceDays", "preclosureAllowed", "lockInMonths", + "interestOnPreclosure", "revaluationFreqMonths", "insuranceRequired", "stockStatementFreqMonths", + "chequeBookEnabled", "chequeType", "defaultChequeBookSize", "ppsThreshold", "ppsMandatoryAbove", + "shareCapitalGl", + // security & lien extras + "eligibleDepositProducts", "lienTenureNotExceedMaturity", "lienAutoReleaseOnClosure", + "lienAllowMultipleDeposits", "lienAdjustFromMaturity", "lienAllowThirdParty", "lienAllowRenewalUnderLien", + // limit & tenure extras + "repaymentType", "appropriationOrder", "debitBalanceAllow", "allowDebitAfterDueDate", "branchLevelAccountType", + // interest extras + "interestCalcMethod", "interestOnCreditBalance", "interestPostingDay", + // NPA upgrade rules + "npaUpgradeOnlyAllArrears", "npaKeepStandardIfDepositCovers", + // cheque book full config + "chequeBookSizes", "chequeMaxBooksOutstanding", "chequeReissueWhenLeaves", "chequeFirstBookFree", + "chequeFreeBooksPerYear", "chequeNamePrinted", "chequePrintDescription", "chequeStopPaymentAllowed", + "chequeMaintainStockRegister", "chequeAllowWithdrawalSlip", "chequeBlockNpa", "chequeBlockLimitExpired", + "chequeBlockOverDp", "chequeBlockFrozen", "chequeBlockLienReleased" }; + + /** + * Insert or partial-update the satellite for this product. Returns the fields that actually changed so the + * caller can fold them into the loan-product audit {@code changes} map. + */ + public Map saveOrUpdate(final LoanProduct loanProduct, final JsonCommand command) { + final Map changes = new LinkedHashMap<>(); + if (!hasAnyAdvanceParameter(command)) { + return changes; + } + final LoanProductAdvanceExt ext = this.repository.findByLoanProductId(loanProduct.getId()) + .orElseGet(() -> new LoanProductAdvanceExt(loanProduct)); + applyScalarFields(ext, command, changes); + this.repository.save(ext); + return changes; + } + + public Map retrieveAsMap(final Long loanProductId) { + return this.repository.findByLoanProductId(loanProductId).map(LoanProductAdvanceExtService::toMap) + .orElseGet(LinkedHashMap::new); + } + + private boolean hasAnyAdvanceParameter(final JsonCommand command) { + for (final String param : ADVANCE_PARAMETERS) { + if (command.parameterExists(param)) { + return true; + } + } + return false; + } + + private void applyScalarFields(final LoanProductAdvanceExt ext, final JsonCommand command, + final Map changes) { + setString(command, "productTypeGroup", ext::getProductTypeGroup, ext::setProductTypeGroup, changes); + setString(command, "productTypeSubGroup", ext::getProductTypeSubGroup, ext::setProductTypeSubGroup, changes); + setLong(command, "glAccount", ext::getGlAccountId, ext::setGlAccountId, changes); + setString(command, "loanProductCategory", ext::getLoanProductCategory, ext::setLoanProductCategory, changes); + setString(command, "prioritySector", ext::getPrioritySector, ext::setPrioritySector, changes); + setString(command, "cibilAccountType", ext::getCibilAccountType, ext::setCibilAccountType, changes); + setBoolean(command, "kfsEnabled", ext::getKfsEnabled, ext::setKfsEnabled, changes); + setBoolean(command, "aprEnabled", ext::getAprEnabled, ext::setAprEnabled, changes); + setString(command, "facilityType", ext::getFacilityType, ext::setFacilityType, changes); + setString(command, "securityType", ext::getSecurityType, ext::setSecurityType, changes); + setString(command, "chargeType", ext::getChargeType, ext::setChargeType, changes); + setString(command, "eligibleAmountBasis", ext::getEligibleAmountBasis, ext::setEligibleAmountBasis, changes); + setDecimal(command, "margin", ext::getMarginPct, ext::setMarginPct, changes); + setDecimal(command, "ltv", ext::getLtvPct, ext::setLtvPct, changes); + setString(command, "dpMethod", ext::getDpMethod, ext::setDpMethod, changes); + setBoolean(command, "lienRequired", ext::getLienRequired, ext::setLienRequired, changes); + setString(command, "lienMarkingStage", ext::getLienMarkingStage, ext::setLienMarkingStage, changes); + setBoolean(command, "lienCertificateRequired", ext::getLienCertificateRequired, ext::setLienCertificateRequired, + changes); + setString(command, "lienCertificatePrefix", ext::getLienCertificatePrefix, ext::setLienCertificatePrefix, + changes); + setBoolean(command, "printCertificateOnDisbursement", ext::getPrintCertificateOnDisbursement, + ext::setPrintCertificateOnDisbursement, changes); + setString(command, "interestBasis", ext::getInterestBasis, ext::setInterestBasis, changes); + setDecimal(command, "spread", ext::getSpreadPct, ext::setSpreadPct, changes); + setString(command, "interestChargedOn", ext::getInterestChargedOn, ext::setInterestChargedOn, changes); + setString(command, "interestPostingFrequency", ext::getInterestPostingFrequency, + ext::setInterestPostingFrequency, changes); + setBoolean(command, "penalApplicable", ext::getPenalApplicable, ext::setPenalApplicable, changes); + setDecimal(command, "penalRate", ext::getPenalRatePct, ext::setPenalRatePct, changes); + setString(command, "penalAppliedOn", ext::getPenalAppliedOn, ext::setPenalAppliedOn, changes); + setInteger(command, "penalGraceDays", ext::getPenalGraceDays, ext::setPenalGraceDays, changes); + setString(command, "npaTrigger", ext::getNpaTrigger, ext::setNpaTrigger, changes); + setString(command, "interestAfterNpa", ext::getInterestAfterNpa, ext::setInterestAfterNpa, changes); + setLong(command, "undebitedInterestGlId", ext::getUndebitedInterestGlId, ext::setUndebitedInterestGlId, changes); + setBoolean(command, "renewalAllowed", ext::getRenewalAllowed, ext::setRenewalAllowed, changes); + setInteger(command, "renewalPeriodMonths", ext::getRenewalPeriodMonths, ext::setRenewalPeriodMonths, changes); + setInteger(command, "renewalGraceDays", ext::getRenewalGraceDays, ext::setRenewalGraceDays, changes); + setString(command, "preclosureAllowed", ext::getPreclosureAllowed, ext::setPreclosureAllowed, changes); + setInteger(command, "lockInMonths", ext::getLockInMonths, ext::setLockInMonths, changes); + setString(command, "interestOnPreclosure", ext::getInterestOnPreclosure, ext::setInterestOnPreclosure, changes); + setInteger(command, "revaluationFreqMonths", ext::getRevaluationFreqMonths, ext::setRevaluationFreqMonths, + changes); + setBoolean(command, "insuranceRequired", ext::getInsuranceRequired, ext::setInsuranceRequired, changes); + setInteger(command, "stockStatementFreqMonths", ext::getStockStatementFreqMonths, + ext::setStockStatementFreqMonths, changes); + setBoolean(command, "chequeBookEnabled", ext::getChequeBookEnabled, ext::setChequeBookEnabled, changes); + setString(command, "chequeType", ext::getChequeType, ext::setChequeType, changes); + setInteger(command, "defaultChequeBookSize", ext::getDefaultChequeBookSize, ext::setDefaultChequeBookSize, + changes); + setDecimal(command, "ppsThreshold", ext::getPpsThreshold, ext::setPpsThreshold, changes); + setDecimal(command, "ppsMandatoryAbove", ext::getPpsMandatoryAbove, ext::setPpsMandatoryAbove, changes); + setLong(command, "shareCapitalGl", ext::getShareCapitalGlId, ext::setShareCapitalGlId, changes); + // security & lien extras + setString(command, "eligibleDepositProducts", ext::getEligibleDepositProducts, ext::setEligibleDepositProducts, + changes); + setBoolean(command, "lienTenureNotExceedMaturity", ext::getLienTenureNotExceedMaturity, + ext::setLienTenureNotExceedMaturity, changes); + setBoolean(command, "lienAutoReleaseOnClosure", ext::getLienAutoReleaseOnClosure, + ext::setLienAutoReleaseOnClosure, changes); + setBoolean(command, "lienAllowMultipleDeposits", ext::getLienAllowMultipleDeposits, + ext::setLienAllowMultipleDeposits, changes); + setBoolean(command, "lienAdjustFromMaturity", ext::getLienAdjustFromMaturity, ext::setLienAdjustFromMaturity, + changes); + setBoolean(command, "lienAllowThirdParty", ext::getLienAllowThirdParty, ext::setLienAllowThirdParty, changes); + setBoolean(command, "lienAllowRenewalUnderLien", ext::getLienAllowRenewalUnderLien, + ext::setLienAllowRenewalUnderLien, changes); + // limit & tenure extras + setString(command, "repaymentType", ext::getRepaymentType, ext::setRepaymentType, changes); + setString(command, "appropriationOrder", ext::getAppropriationOrder, ext::setAppropriationOrder, changes); + setBoolean(command, "debitBalanceAllow", ext::getDebitBalanceAllow, ext::setDebitBalanceAllow, changes); + setBoolean(command, "allowDebitAfterDueDate", ext::getAllowDebitAfterDueDate, ext::setAllowDebitAfterDueDate, + changes); + setBoolean(command, "branchLevelAccountType", ext::getBranchLevelAccountType, ext::setBranchLevelAccountType, + changes); + // interest extras + setString(command, "interestCalcMethod", ext::getInterestCalcMethod, ext::setInterestCalcMethod, changes); + setBoolean(command, "interestOnCreditBalance", ext::getInterestOnCreditBalance, ext::setInterestOnCreditBalance, + changes); + setString(command, "interestPostingDay", ext::getInterestPostingDay, ext::setInterestPostingDay, changes); + // NPA upgrade rules + setBoolean(command, "npaUpgradeOnlyAllArrears", ext::getNpaUpgradeOnlyAllArrears, + ext::setNpaUpgradeOnlyAllArrears, changes); + setBoolean(command, "npaKeepStandardIfDepositCovers", ext::getNpaKeepStandardIfDepositCovers, + ext::setNpaKeepStandardIfDepositCovers, changes); + // cheque book full config + setString(command, "chequeBookSizes", ext::getChequeBookSizes, ext::setChequeBookSizes, changes); + setInteger(command, "chequeMaxBooksOutstanding", ext::getChequeMaxBooksOutstanding, + ext::setChequeMaxBooksOutstanding, changes); + setInteger(command, "chequeReissueWhenLeaves", ext::getChequeReissueWhenLeaves, ext::setChequeReissueWhenLeaves, + changes); + setBoolean(command, "chequeFirstBookFree", ext::getChequeFirstBookFree, ext::setChequeFirstBookFree, changes); + setInteger(command, "chequeFreeBooksPerYear", ext::getChequeFreeBooksPerYear, ext::setChequeFreeBooksPerYear, + changes); + setString(command, "chequeNamePrinted", ext::getChequeNamePrinted, ext::setChequeNamePrinted, changes); + setString(command, "chequePrintDescription", ext::getChequePrintDescription, ext::setChequePrintDescription, + changes); + setBoolean(command, "chequeStopPaymentAllowed", ext::getChequeStopPaymentAllowed, + ext::setChequeStopPaymentAllowed, changes); + setBoolean(command, "chequeMaintainStockRegister", ext::getChequeMaintainStockRegister, + ext::setChequeMaintainStockRegister, changes); + setBoolean(command, "chequeAllowWithdrawalSlip", ext::getChequeAllowWithdrawalSlip, + ext::setChequeAllowWithdrawalSlip, changes); + setBoolean(command, "chequeBlockNpa", ext::getChequeBlockNpa, ext::setChequeBlockNpa, changes); + setBoolean(command, "chequeBlockLimitExpired", ext::getChequeBlockLimitExpired, ext::setChequeBlockLimitExpired, + changes); + setBoolean(command, "chequeBlockOverDp", ext::getChequeBlockOverDp, ext::setChequeBlockOverDp, changes); + setBoolean(command, "chequeBlockFrozen", ext::getChequeBlockFrozen, ext::setChequeBlockFrozen, changes); + setBoolean(command, "chequeBlockLienReleased", ext::getChequeBlockLienReleased, ext::setChequeBlockLienReleased, + changes); + } + + // ---- typed setters: only write params that are present, and only report real changes ---------- + + private interface Getter { + + T get(); + } + + private interface Ser { + + void set(T value); + } + + private void putIfChanged(final String param, final T oldValue, final T newValue, final Ser setter, + final Map changes) { + if (differs(oldValue, newValue)) { + setter.set(newValue); + changes.put(param, newValue); + } + } + + private void setString(final JsonCommand command, final String param, final Getter getter, + final Ser setter, final Map changes) { + if (command.parameterExists(param)) { + putIfChanged(param, getter.get(), command.stringValueOfParameterNamedAllowingNull(param), setter, changes); + } + } + + private void setLong(final JsonCommand command, final String param, final Getter getter, + final Ser setter, final Map changes) { + if (command.parameterExists(param)) { + putIfChanged(param, getter.get(), command.longValueOfParameterNamed(param), setter, changes); + } + } + + private void setInteger(final JsonCommand command, final String param, final Getter getter, + final Ser setter, final Map changes) { + if (command.parameterExists(param)) { + putIfChanged(param, getter.get(), command.integerValueOfParameterNamed(param), setter, changes); + } + } + + private void setDecimal(final JsonCommand command, final String param, final Getter getter, + final Ser setter, final Map changes) { + if (command.parameterExists(param)) { + putIfChanged(param, getter.get(), command.bigDecimalValueOfParameterNamed(param), setter, changes); + } + } + + /** + * The form stores its yes/no dropdowns as the strings "YES"/"NO" and its checkboxes as real booleans; + * accept both shapes so a "NO" is not silently read as null. + */ + private void setBoolean(final JsonCommand command, final String param, final Getter getter, + final Ser setter, final Map changes) { + if (command.parameterExists(param)) { + putIfChanged(param, getter.get(), asBoolean(command.jsonElement(param)), setter, changes); + } + } + + private static Boolean asBoolean(final JsonElement element) { + if (element == null || element.isJsonNull() || !element.isJsonPrimitive()) { + return null; + } + if (element.getAsJsonPrimitive().isBoolean()) { + return element.getAsBoolean(); + } + final String raw = element.getAsString(); + if (raw == null || raw.isBlank()) { + return null; + } + return "true".equalsIgnoreCase(raw) || "yes".equalsIgnoreCase(raw) || "1".equals(raw); + } + + /** BigDecimal.equals compares scale, so 2.0 vs 2.00 would read as a change; compareTo does not. */ + private static boolean differs(final Object oldValue, final Object newValue) { + if (oldValue instanceof BigDecimal a && newValue instanceof BigDecimal b) { + return a.compareTo(b) != 0; + } + return !Objects.equals(oldValue, newValue); + } + + private static Map toMap(final LoanProductAdvanceExt ext) { + final Map map = new LinkedHashMap<>(); + map.put("productTypeGroup", ext.getProductTypeGroup()); + map.put("productTypeSubGroup", ext.getProductTypeSubGroup()); + map.put("glAccount", ext.getGlAccountId()); + map.put("loanProductCategory", ext.getLoanProductCategory()); + map.put("prioritySector", ext.getPrioritySector()); + map.put("cibilAccountType", ext.getCibilAccountType()); + map.put("kfsEnabled", ext.getKfsEnabled()); + map.put("aprEnabled", ext.getAprEnabled()); + map.put("facilityType", ext.getFacilityType()); + map.put("securityType", ext.getSecurityType()); + map.put("chargeType", ext.getChargeType()); + map.put("eligibleAmountBasis", ext.getEligibleAmountBasis()); + map.put("margin", ext.getMarginPct()); + map.put("ltv", ext.getLtvPct()); + map.put("dpMethod", ext.getDpMethod()); + map.put("lienRequired", ext.getLienRequired()); + map.put("lienMarkingStage", ext.getLienMarkingStage()); + map.put("lienCertificateRequired", ext.getLienCertificateRequired()); + map.put("lienCertificatePrefix", ext.getLienCertificatePrefix()); + map.put("printCertificateOnDisbursement", ext.getPrintCertificateOnDisbursement()); + map.put("interestBasis", ext.getInterestBasis()); + map.put("spread", ext.getSpreadPct()); + map.put("interestChargedOn", ext.getInterestChargedOn()); + map.put("interestPostingFrequency", ext.getInterestPostingFrequency()); + map.put("penalApplicable", ext.getPenalApplicable()); + map.put("penalRate", ext.getPenalRatePct()); + map.put("penalAppliedOn", ext.getPenalAppliedOn()); + map.put("penalGraceDays", ext.getPenalGraceDays()); + map.put("npaTrigger", ext.getNpaTrigger()); + map.put("interestAfterNpa", ext.getInterestAfterNpa()); + map.put("undebitedInterestGlId", ext.getUndebitedInterestGlId()); + map.put("renewalAllowed", ext.getRenewalAllowed()); + map.put("renewalPeriodMonths", ext.getRenewalPeriodMonths()); + map.put("renewalGraceDays", ext.getRenewalGraceDays()); + map.put("preclosureAllowed", ext.getPreclosureAllowed()); + map.put("lockInMonths", ext.getLockInMonths()); + map.put("interestOnPreclosure", ext.getInterestOnPreclosure()); + map.put("revaluationFreqMonths", ext.getRevaluationFreqMonths()); + map.put("insuranceRequired", ext.getInsuranceRequired()); + map.put("stockStatementFreqMonths", ext.getStockStatementFreqMonths()); + map.put("chequeBookEnabled", ext.getChequeBookEnabled()); + map.put("chequeType", ext.getChequeType()); + map.put("defaultChequeBookSize", ext.getDefaultChequeBookSize()); + map.put("ppsThreshold", ext.getPpsThreshold()); + map.put("ppsMandatoryAbove", ext.getPpsMandatoryAbove()); + map.put("shareCapitalGl", ext.getShareCapitalGlId()); + map.put("eligibleDepositProducts", ext.getEligibleDepositProducts()); + map.put("lienTenureNotExceedMaturity", ext.getLienTenureNotExceedMaturity()); + map.put("lienAutoReleaseOnClosure", ext.getLienAutoReleaseOnClosure()); + map.put("lienAllowMultipleDeposits", ext.getLienAllowMultipleDeposits()); + map.put("lienAdjustFromMaturity", ext.getLienAdjustFromMaturity()); + map.put("lienAllowThirdParty", ext.getLienAllowThirdParty()); + map.put("lienAllowRenewalUnderLien", ext.getLienAllowRenewalUnderLien()); + map.put("repaymentType", ext.getRepaymentType()); + map.put("appropriationOrder", ext.getAppropriationOrder()); + map.put("debitBalanceAllow", ext.getDebitBalanceAllow()); + map.put("allowDebitAfterDueDate", ext.getAllowDebitAfterDueDate()); + map.put("branchLevelAccountType", ext.getBranchLevelAccountType()); + map.put("interestCalcMethod", ext.getInterestCalcMethod()); + map.put("interestOnCreditBalance", ext.getInterestOnCreditBalance()); + map.put("interestPostingDay", ext.getInterestPostingDay()); + map.put("npaUpgradeOnlyAllArrears", ext.getNpaUpgradeOnlyAllArrears()); + map.put("npaKeepStandardIfDepositCovers", ext.getNpaKeepStandardIfDepositCovers()); + map.put("chequeBookSizes", ext.getChequeBookSizes()); + map.put("chequeMaxBooksOutstanding", ext.getChequeMaxBooksOutstanding()); + map.put("chequeReissueWhenLeaves", ext.getChequeReissueWhenLeaves()); + map.put("chequeFirstBookFree", ext.getChequeFirstBookFree()); + map.put("chequeFreeBooksPerYear", ext.getChequeFreeBooksPerYear()); + map.put("chequeNamePrinted", ext.getChequeNamePrinted()); + map.put("chequePrintDescription", ext.getChequePrintDescription()); + map.put("chequeStopPaymentAllowed", ext.getChequeStopPaymentAllowed()); + map.put("chequeMaintainStockRegister", ext.getChequeMaintainStockRegister()); + map.put("chequeAllowWithdrawalSlip", ext.getChequeAllowWithdrawalSlip()); + map.put("chequeBlockNpa", ext.getChequeBlockNpa()); + map.put("chequeBlockLimitExpired", ext.getChequeBlockLimitExpired()); + map.put("chequeBlockOverDp", ext.getChequeBlockOverDp()); + map.put("chequeBlockFrozen", ext.getChequeBlockFrozen()); + map.put("chequeBlockLienReleased", ext.getChequeBlockLienReleased()); + return map; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 3b78f94d0..08cff1172 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -97,6 +97,8 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro private final CreditAllocationsJsonParser creditAllocationsJsonParser; private final LoanProductAssembler loanProductAssembler; private final LoanProductUpdateUtil loanProductUpdateUtil; + // "Advances A/C" satellite fields (security, lien, renewal, npa, cheque book) that m_product_loan has no home for. + private final LoanProductAdvanceExtService loanProductAdvanceExtService; private final LoanProductPaymentAllocationRuleMerger loanProductPaymentAllocationRuleMerger = new LoanProductPaymentAllocationRuleMerger(); private final LoanProductCreditAllocationRuleMerger loanProductCreditAllocationRuleMerger = new LoanProductCreditAllocationRuleMerger(); @@ -141,6 +143,10 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro this.loanProductRepository.saveAndFlush(loanProduct); + // "Advances A/C" satellite fields, keyed on the product's generated id. A standard product carries + // none of these parameters and writes no row. Same transaction: a failure rolls the product back. + this.loanProductAdvanceExtService.saveOrUpdate(loanProduct, command); + // save accounting mappings this.accountMappingWritePlatformService.createLoanProductToGLAccountMapping(loanProduct.getId(), command); // check if the office specific products are enabled. If yes, then @@ -303,6 +309,11 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro this.loanProductRepository.saveAndFlush(product); } + // "Advances A/C" satellite fields. Outside the changes check above (those track only the standard + // product columns) so an edit that touched only an advance field is still saved, and its per-field + // diff is folded into the same audit changes map. + changes.putAll(this.loanProductAdvanceExtService.saveOrUpdate(product, command)); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanProductId) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java index e5617d0c1..a791d9ac8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/starter/LoanProductConfiguration.java @@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatfor import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformServiceImpl; import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformService; +import org.apache.fineract.portfolio.loanproduct.service.LoanProductAdvanceExtService; import org.apache.fineract.portfolio.loanproduct.service.LoanProductWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.rate.domain.RateRepositoryWrapper; import org.apache.fineract.portfolio.rate.service.RateReadService; @@ -82,11 +83,12 @@ public class LoanProductConfiguration { DelinquencyBucketRepository delinquencyBucketRepository, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, AdvancedPaymentAllocationsJsonParser advancedPaymentJsonParser, CreditAllocationsJsonParser creditAllocationsJsonParser, - LoanProductAssembler loanProductAssembler, LoanProductUpdateUtil loanProductUpdateUtil) { + LoanProductAssembler loanProductAssembler, LoanProductUpdateUtil loanProductUpdateUtil, + LoanProductAdvanceExtService loanProductAdvanceExtService) { return new LoanProductWritePlatformServiceJpaRepositoryImpl(context, fromApiJsonDeserializer, loanProductRepository, aprCalculator, fundRepository, chargeRepository, rateRepository, accountMappingWritePlatformService, fineractEntityAccessUtil, floatingRateRepository, loanRepositoryWrapper, businessEventNotifierService, delinquencyBucketRepository, loanRepaymentScheduleTransactionProcessorFactory, advancedPaymentJsonParser, creditAllocationsJsonParser, - loanProductAssembler, loanProductUpdateUtil); + loanProductAssembler, loanProductUpdateUtil, loanProductAdvanceExtService); } } 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 b77490e94..ed7e1ca26 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 @@ -615,4 +615,6 @@ + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_create_loan_product_advance_ext.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_create_loan_product_advance_ext.xml new file mode 100644 index 000000000..f5653ca29 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0571_create_loan_product_advance_ext.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_loan_product_advance_ext_fields.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_loan_product_advance_ext_fields.xml new file mode 100644 index 000000000..9fa3f130d --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0572_add_loan_product_advance_ext_fields.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +