Compare commits
4
Commits
main
...
39e3fcec59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e3fcec59 | ||
|
|
b726577031 | ||
|
|
d1e94fd6a3 | ||
|
|
22c4403a14 |
+8
@@ -2227,6 +2227,14 @@ public class CommandWrapperBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder createCbsLoanBaseInterestRate() {
|
||||
this.actionName = "CREATE";
|
||||
this.entityName = "CBSLOANBASEINTERESTRATE";
|
||||
this.entityId = null;
|
||||
this.href = "/loanproducts/baseinterestrates";
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandWrapperBuilder updateFixedDepositProduct(final Long productId) {
|
||||
this.actionName = "UPDATE";
|
||||
this.entityName = "FIXEDDEPOSITPRODUCT";
|
||||
|
||||
+1
@@ -505,6 +505,7 @@ public class FineractProperties {
|
||||
private FineractTransactionProcessorItemProperties heavensFamily;
|
||||
private FineractTransactionProcessorItemProperties interestPrincipalPenaltiesFees;
|
||||
private FineractTransactionProcessorItemProperties principalInterestPenaltiesFees;
|
||||
private FineractTransactionProcessorItemProperties feesPenaltiesInterestPrincipal;
|
||||
private FineractTransactionProcessorItemProperties rbiIndia;
|
||||
private FineractTransactionProcessorItemProperties duePenaltyFeeInterestPrincipalInAdvancePrincipalPenaltyFeeInterest;
|
||||
private FineractTransactionProcessorItemProperties duePenaltyInterestPrincipalFeeInAdvancePenaltyInterestPrincipalFee;
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
|
||||
/**
|
||||
* {@link LoanRepaymentScheduleTransactionProcessor} that pays off components in order of fees, penalties, interest,
|
||||
* then principal.
|
||||
*
|
||||
* Other formulas exist on fineract where you can choose 'Declining-Balance Interest Recalculation' which simply means,
|
||||
* recalculate the interest component based on the how much principal is outstanding at a point in time; but this isnt
|
||||
* trying to model that option only the basic one for now.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
|
||||
|
||||
public static final String STRATEGY_CODE = "fees-penalties-interest-principal-order-strategy";
|
||||
|
||||
public static final String STRATEGY_NAME = "Fees, Penalties, Interest, Principal Order";
|
||||
|
||||
public FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor(final ExternalIdFactory externalIdFactory,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return STRATEGY_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return STRATEGY_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* For early/'in advance' repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction, final Money paymentInAdvance,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, transactionMappings,
|
||||
charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For late repayments, pay off in the same way as on-time payments, interest first then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
|
||||
final Money transactionAmountUnprocessed, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges) {
|
||||
|
||||
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed,
|
||||
transactionMappings, charges);
|
||||
}
|
||||
|
||||
/**
|
||||
* For normal on-time repayments, pays off interest first, then principal.
|
||||
*/
|
||||
@Override
|
||||
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
|
||||
|
||||
if (loanTransaction.isChargesWaiver()) {
|
||||
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
|
||||
loanTransaction.getPenaltyChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate,
|
||||
loanTransaction.getFeeChargesPortion(currency));
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
} else if (loanTransaction.isInterestWaiver()) {
|
||||
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else if (loanTransaction.isChargePayment()) {
|
||||
if (loanTransaction.isPenaltyPayment()) {
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
} else {
|
||||
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
|
||||
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
|
||||
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
|
||||
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
}
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
|
||||
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings) {
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
|
||||
Money transactionAmountRemaining = transactionAmountUnprocessed;
|
||||
Money principalPortion = Money.zero(currency);
|
||||
Money interestPortion = Money.zero(currency);
|
||||
Money feeChargesPortion = Money.zero(currency);
|
||||
Money penaltyChargesPortion = Money.zero(currency);
|
||||
|
||||
principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
|
||||
}
|
||||
|
||||
if (transactionAmountRemaining.isGreaterThanZero()) {
|
||||
feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining);
|
||||
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
|
||||
}
|
||||
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
|
||||
if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) {
|
||||
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment,
|
||||
principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion));
|
||||
}
|
||||
return transactionAmountRemaining;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.starter;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.condition.PropertiesCondition;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
|
||||
public class FeesPenaltiesInterestPrincipalLoanRepaymentScheduleTransactionProcessorCondition extends PropertiesCondition {
|
||||
|
||||
@Override
|
||||
protected boolean matches(FineractProperties properties) {
|
||||
return properties.getLoan().getTransactionProcessor().getFeesPenaltiesInterestPrincipal().isEnabled();
|
||||
}
|
||||
}
|
||||
+14
@@ -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<String, Object> 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<String, Object> advanceExtensions) {
|
||||
this.advanceExtensions = advanceExtensions;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAdvanceExtensions() {
|
||||
return this.advanceExtensions;
|
||||
}
|
||||
|
||||
public boolean isIsLinkedToFloatingInterestRates() {
|
||||
return this.isLinkedToFloatingInterestRates;
|
||||
}
|
||||
|
||||
+302
@@ -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();
|
||||
}
|
||||
}
|
||||
+16
@@ -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<LoanProductAdvanceExt, Long> {
|
||||
|
||||
Optional<LoanProductAdvanceExt> findByLoanProductId(Long loanProductId);
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* One CIBIL/credit-score band of a rate register version — e.g. {601..650} tier "Fair" adds +1.00%.
|
||||
* Applied on top of the chart cell at loan sanction. Typically 5 rows per version; ordered on reads
|
||||
* by {@code displayOrder}.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_loan_rate_register_cibil_slab")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class LoanRateRegisterCibilSlab {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "effective_id", nullable = false)
|
||||
private Long effectiveId;
|
||||
|
||||
@Column(name = "score_from", nullable = false)
|
||||
private Integer scoreFrom;
|
||||
|
||||
@Column(name = "score_to", nullable = false)
|
||||
private Integer scoreTo;
|
||||
|
||||
@Column(name = "tier", length = 30, nullable = false)
|
||||
private String tier;
|
||||
|
||||
@Column(name = "slab_type", length = 20, nullable = false)
|
||||
private String slabType;
|
||||
|
||||
@Column(name = "adjustment_pct", nullable = false)
|
||||
private BigDecimal adjustmentPct;
|
||||
|
||||
@Column(name = "display_order", nullable = false)
|
||||
private Integer displayOrder;
|
||||
|
||||
@Column(name = "created_date")
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public static LoanRateRegisterCibilSlab create(final Long effectiveId, final Integer scoreFrom, final Integer scoreTo, final String tier,
|
||||
final String slabType, final BigDecimal adjustmentPct, final Integer displayOrder) {
|
||||
final LoanRateRegisterCibilSlab s = new LoanRateRegisterCibilSlab();
|
||||
s.effectiveId = effectiveId;
|
||||
s.scoreFrom = scoreFrom;
|
||||
s.scoreTo = scoreTo;
|
||||
s.tier = tier;
|
||||
s.slabType = slabType != null ? slabType : "Flat";
|
||||
s.adjustmentPct = adjustmentPct;
|
||||
s.displayOrder = displayOrder;
|
||||
s.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
return s;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LoanRateRegisterCibilSlabRepository extends JpaRepository<LoanRateRegisterCibilSlab, Long> {
|
||||
|
||||
/** CIBIL bands for a version, ordered as the UI shows them (ascending score bands). */
|
||||
List<LoanRateRegisterCibilSlab> findByEffectiveIdOrderByDisplayOrderAsc(Long effectiveId);
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* One effective (versioned) copy of the central Loan Interest Rate Register. Mirrors the
|
||||
* {@code FdBaseInterestRateEffective} versioning model: each approved save creates a new row with
|
||||
* {@code isActive=true} and deactivates the previous active row ({@code isActive=false,
|
||||
* effectiveToDate=today}). Reads for "current active" filter on {@code isActive=true}.
|
||||
*
|
||||
* The 34-product × 10-slab rate matrix for this version lives in {@link LoanRateRegisterSlabHistory};
|
||||
* the CIBIL/credit-score bands live in {@link LoanRateRegisterCibilSlab}, both keyed by this row's
|
||||
* id and cascade-deleted with it.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_loan_rate_register_effective")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class LoanRateRegisterEffective {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name", length = 200, nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "effective_from_date", nullable = false)
|
||||
private LocalDate effectiveFromDate;
|
||||
|
||||
@Column(name = "effective_to_date")
|
||||
private LocalDate effectiveToDate;
|
||||
|
||||
@Column(name = "currency_code", length = 3, nullable = false)
|
||||
private String currencyCode;
|
||||
|
||||
@Column(name = "is_active", nullable = false)
|
||||
private Boolean isActive;
|
||||
|
||||
@Column(name = "fallback_no_cibil_slab", nullable = false)
|
||||
private Integer fallbackNoCibilSlab;
|
||||
|
||||
@Column(name = "fallback_below_min", length = 20, nullable = false)
|
||||
private String fallbackBelowMin;
|
||||
|
||||
@Column(name = "note", length = 500)
|
||||
private String note;
|
||||
|
||||
@Column(name = "created_by_id")
|
||||
private Long createdById;
|
||||
|
||||
@Column(name = "created_date")
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public static LoanRateRegisterEffective create(final String name, final LocalDate effectiveFromDate, final String currencyCode,
|
||||
final Integer fallbackNoCibilSlab, final String fallbackBelowMin, final String note, final Long createdById) {
|
||||
final LoanRateRegisterEffective e = new LoanRateRegisterEffective();
|
||||
e.name = name;
|
||||
e.effectiveFromDate = effectiveFromDate;
|
||||
e.currencyCode = currencyCode != null ? currencyCode : "INR";
|
||||
e.isActive = true;
|
||||
e.fallbackNoCibilSlab = fallbackNoCibilSlab != null ? fallbackNoCibilSlab : 2;
|
||||
e.fallbackBelowMin = fallbackBelowMin != null ? fallbackBelowMin : "block";
|
||||
e.note = note;
|
||||
e.createdById = createdById;
|
||||
e.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Flip this version to inactive and close its effective window. Called when a newer version is approved. */
|
||||
public void deactivate(final LocalDate toDate) {
|
||||
this.isActive = false;
|
||||
this.effectiveToDate = toDate;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LoanRateRegisterEffectiveRepository extends JpaRepository<LoanRateRegisterEffective, Long> {
|
||||
|
||||
/** All active versions, newest-effective first. At most one row in a healthy system. */
|
||||
List<LoanRateRegisterEffective> findByIsActiveTrueOrderByEffectiveFromDateDesc();
|
||||
|
||||
/** The current active version, if any. */
|
||||
Optional<LoanRateRegisterEffective> findFirstByIsActiveTrueOrderByEffectiveFromDateDesc();
|
||||
|
||||
/** Version history list — all versions, newest first. */
|
||||
List<LoanRateRegisterEffective> findAllByOrderByEffectiveFromDateDesc();
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
|
||||
/**
|
||||
* A single cell of a rate register version — one row per {product × amount-slab}. Product and slab
|
||||
* metadata are denormalised so one query can rebuild the full matrix without a join. The cell holds
|
||||
* either a plain numeric rate, a formula string ({@code "1% + FD"}) resolved at sanction, or
|
||||
* N.A. (both value and formula null, {@code isNa=true}) to mean "slab not offered".
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "m_loan_rate_register_slab_history")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class LoanRateRegisterSlabHistory {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "effective_id", nullable = false)
|
||||
private Long effectiveId;
|
||||
|
||||
// ---- product dimension ----------------------------------------------
|
||||
@Column(name = "product_sr_no", nullable = false)
|
||||
private Integer productSrNo;
|
||||
|
||||
@Column(name = "product_name", length = 200, nullable = false)
|
||||
private String productName;
|
||||
|
||||
// ---- amount-slab dimension ------------------------------------------
|
||||
@Column(name = "slab_order", nullable = false)
|
||||
private Integer slabOrder;
|
||||
|
||||
@Column(name = "slab_label", length = 60, nullable = false)
|
||||
private String slabLabel;
|
||||
|
||||
@Column(name = "amount_upto")
|
||||
private BigDecimal amountUpto;
|
||||
|
||||
// ---- cell value (exactly one of value / formula is populated; both null + isNa=true means N.A.) ----
|
||||
@Column(name = "rate_value")
|
||||
private BigDecimal rateValue;
|
||||
|
||||
@Column(name = "rate_formula", length = 50)
|
||||
private String rateFormula;
|
||||
|
||||
@Column(name = "is_formula", nullable = false)
|
||||
private Boolean isFormula;
|
||||
|
||||
@Column(name = "is_na", nullable = false)
|
||||
private Boolean isNa;
|
||||
|
||||
@Column(name = "created_date")
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
public static LoanRateRegisterSlabHistory create(final Long effectiveId, final Integer productSrNo, final String productName,
|
||||
final Integer slabOrder, final String slabLabel, final BigDecimal amountUpto, final BigDecimal rateValue,
|
||||
final String rateFormula, final boolean isFormula, final boolean isNa) {
|
||||
final LoanRateRegisterSlabHistory s = new LoanRateRegisterSlabHistory();
|
||||
s.effectiveId = effectiveId;
|
||||
s.productSrNo = productSrNo;
|
||||
s.productName = productName;
|
||||
s.slabOrder = slabOrder;
|
||||
s.slabLabel = slabLabel;
|
||||
s.amountUpto = amountUpto;
|
||||
s.rateValue = rateValue;
|
||||
s.rateFormula = rateFormula;
|
||||
s.isFormula = isFormula;
|
||||
s.isNa = isNa;
|
||||
s.createdDate = LocalDateTime.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
return s;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface LoanRateRegisterSlabHistoryRepository extends JpaRepository<LoanRateRegisterSlabHistory, Long> {
|
||||
|
||||
/** All cells of a version, ordered by product then by amount-slab. Rebuilds the full matrix in order. */
|
||||
List<LoanRateRegisterSlabHistory> findByEffectiveIdOrderByProductSrNoAscSlabOrderAsc(Long effectiveId);
|
||||
}
|
||||
+10
@@ -27,6 +27,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
|
||||
@@ -98,6 +99,15 @@ public class LoanAccountAutoStarter {
|
||||
loanBalanceService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Conditional(FeesPenaltiesInterestPrincipalLoanRepaymentScheduleTransactionProcessorCondition.class)
|
||||
public FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor feesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor(
|
||||
final ExternalIdFactory externalIdFactory, final LoanChargeValidator loanChargeValidator,
|
||||
final LoanBalanceService loanBalanceService) {
|
||||
return new FeesPenaltyInterestPrincipalOrderLoanRepaymentScheduleTransactionProcessor(externalIdFactory, loanChargeValidator,
|
||||
loanBalanceService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Conditional(RBIIndiaLoanRepaymentScheduleTransactionProcessorCondition.class)
|
||||
public RBILoanRepaymentScheduleTransactionProcessor rbiLoanRepaymentScheduleTransactionProcessor(
|
||||
|
||||
+7
-1
@@ -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<String> 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);
|
||||
}
|
||||
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterData;
|
||||
import org.apache.fineract.portfolio.loanproduct.service.LoanRateRegisterReadPlatformService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* REST surface for the Loan Interest Rate Register. Mirrors
|
||||
* {@code FixedDepositProductsBaseInterestRatesApiResource}: a GET list for the Version History
|
||||
* accordion, a GET /active for the current rate chart, a GET /{id} for one specific version, and
|
||||
* a POST that goes through Fineract's maker-checker pipeline.
|
||||
*
|
||||
* Reads are gated by the pre-existing {@code LOANPRODUCT} resource; writes by the two seeded
|
||||
* permissions {@code CREATE_CBSLOANBASEINTERESTRATE} (maker) and {@code CREATE_CBSLOANBASEINTERESTRATE_CHECKER}.
|
||||
*/
|
||||
@Path("/v1/loanproducts/baseinterestrates")
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LoanProductsBaseInterestRatesApiResource {
|
||||
|
||||
private static final String RESOURCE_NAME_FOR_PERMISSIONS = "LOANPRODUCT";
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final LoanRateRegisterReadPlatformService readService;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
|
||||
private final Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(LocalDate.class,
|
||||
(JsonSerializer<LocalDate>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.toString()))
|
||||
.registerTypeAdapter(LocalDateTime.class,
|
||||
(JsonSerializer<LocalDateTime>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))
|
||||
.create();
|
||||
|
||||
/** Full Version History — all effective versions, newest first. */
|
||||
@GET
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get all Loan Interest Rate Register versions", description = "Returns the full history of rate register versions, newest first.")
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
public String retrieveAll() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
final List<LoanRateRegisterData> data = this.readService.retrieveAll();
|
||||
return this.gson.toJson(data);
|
||||
}
|
||||
|
||||
/** The currently in-force version, with its full chart + CIBIL. */
|
||||
@GET
|
||||
@Path("active")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get the active Loan Interest Rate Register", description = "Returns the version flagged isActive=true, or null when none has been approved yet.")
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
public String retrieveActive() {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
final Optional<LoanRateRegisterData> data = this.readService.retrieveActive();
|
||||
return this.gson.toJson(data.orElse(null));
|
||||
}
|
||||
|
||||
/** One specific version by id — used by the Compare screen for the "will be deactivated" baseline. */
|
||||
@GET
|
||||
@Path("{id}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Get a specific rate register version", description = "Returns the full chart + CIBIL bands for the given version id.")
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Not found") })
|
||||
public String retrieveById(@PathParam("id") final Long id) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
final Optional<LoanRateRegisterData> data = this.readService.retrieveById(id);
|
||||
return this.gson.toJson(data.orElse(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a new version. If {@code CREATE_CBSLOANBASEINTERESTRATE.can_maker_checker=true}, this
|
||||
* returns {@code rollbackTransaction=true} and the row sits in {@code m_portfolio_command_source}
|
||||
* until a checker approves it from the Approvals tab. Otherwise it commits immediately.
|
||||
*/
|
||||
@POST
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Submit a new Loan Interest Rate Register version", description = "Creates a new version of the loan interest rate register. Deactivates the current active version on approval.")
|
||||
@ApiResponses({ @ApiResponse(responseCode = "200", description = "OK") })
|
||||
public CommandProcessingResult submitRateRegister(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||
final CommandWrapper commandRequest = new CommandWrapperBuilder()
|
||||
.createCbsLoanBaseInterestRate()
|
||||
.withJson(apiRequestBodyAsJson)
|
||||
.build();
|
||||
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.api;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* JSON shape expected by {@code POST /loanproducts/baseinterestrates}. Deserialised by Gson in the
|
||||
* write service. One request = one proposed new version of the rate register.
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class LoanRateRegisterSaveRequest {
|
||||
|
||||
private String name;
|
||||
/** ISO date "YYYY-MM-DD". Defaults to today when omitted. */
|
||||
private String effectiveFromDate;
|
||||
private String currencyCode;
|
||||
private Integer fallbackNoCibilSlab;
|
||||
private String fallbackBelowMin;
|
||||
private String note;
|
||||
|
||||
private List<SlabRequest> slabs;
|
||||
private List<CibilSlabRequest> cibilSlabs;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public static class SlabRequest {
|
||||
private Integer productSrNo;
|
||||
private String productName;
|
||||
private Integer slabOrder;
|
||||
private String slabLabel;
|
||||
private BigDecimal amountUpto;
|
||||
private BigDecimal rateValue;
|
||||
private String rateFormula;
|
||||
private Boolean isFormula;
|
||||
private Boolean isNa;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public static class CibilSlabRequest {
|
||||
private Integer scoreFrom;
|
||||
private Integer scoreTo;
|
||||
private String tier;
|
||||
private String slabType;
|
||||
private BigDecimal adjustmentPct;
|
||||
private Integer displayOrder;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* One CIBIL/credit-score band of a rate register version, used in the response payload.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class LoanRateRegisterCibilSlabData {
|
||||
|
||||
private Long id;
|
||||
private Integer scoreFrom;
|
||||
private Integer scoreTo;
|
||||
private String tier;
|
||||
private String slabType;
|
||||
private BigDecimal adjustmentPct;
|
||||
private Integer displayOrder;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Response DTO for one effective version of the Loan Interest Rate Register. Header fields +
|
||||
* nested chart cells + CIBIL bands.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class LoanRateRegisterData {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private LocalDate effectiveFromDate;
|
||||
private LocalDate effectiveToDate;
|
||||
private String currencyCode;
|
||||
private Boolean isActive;
|
||||
private Integer fallbackNoCibilSlab;
|
||||
private String fallbackBelowMin;
|
||||
private String note;
|
||||
private Long createdById;
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
private List<LoanRateRegisterSlabData> slabs;
|
||||
private List<LoanRateRegisterCibilSlabData> cibilSlabs;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* One cell of a rate register version — same shape as the entity but a POJO for the response.
|
||||
* Serialised through Gson in the REST resource.
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
public class LoanRateRegisterSlabData {
|
||||
|
||||
private Long id;
|
||||
private Integer productSrNo;
|
||||
private String productName;
|
||||
private Integer slabOrder;
|
||||
private String slabLabel;
|
||||
private BigDecimal amountUpto;
|
||||
private BigDecimal rateValue;
|
||||
private String rateFormula;
|
||||
private Boolean isFormula;
|
||||
private Boolean isNa;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.handler;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.annotation.CommandType;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.portfolio.loanproduct.service.LoanRateRegisterWritePlatformService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Dispatched by Fineract when a {@code CBSLOANBASEINTERESTRATE} / {@code CREATE} command is executed —
|
||||
* on first submit (maker-checker off) or on checker approval (maker-checker on). The {@code @CommandType}
|
||||
* entity string here MUST match the one registered by {@code CommandWrapperBuilder.createCbsLoanBaseInterestRate()}
|
||||
* and the permission codes seeded by {@code 0581_enable_loan_rate_register_maker_checker.xml}.
|
||||
*/
|
||||
@Service
|
||||
@CommandType(entity = "CBSLOANBASEINTERESTRATE", action = "CREATE")
|
||||
@RequiredArgsConstructor
|
||||
public class CreateCbsLoanBaseInterestRateCommandHandler implements NewCommandSourceHandler {
|
||||
|
||||
private final LoanRateRegisterWritePlatformService writePlatformService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||
return this.writePlatformService.saveRateRegister(command);
|
||||
}
|
||||
}
|
||||
+8
@@ -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,
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 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.serialization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest;
|
||||
import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.CibilSlabRequest;
|
||||
import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.SlabRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Validates a {@link LoanRateRegisterSaveRequest} before the write service touches the DB. Keeps
|
||||
* the write service free of guard clauses and surfaces every error up front rather than failing on
|
||||
* the first one — Fineract convention.
|
||||
*/
|
||||
@Component
|
||||
public class LoanRateRegisterDataValidator {
|
||||
|
||||
private static final String RESOURCE_NAME = "loanRateRegister";
|
||||
|
||||
public void validate(final LoanRateRegisterSaveRequest req) {
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
|
||||
if (req == null) {
|
||||
errors.add(error("payload", "payload.required", "Request body is required"));
|
||||
raise(errors);
|
||||
}
|
||||
|
||||
// Header fields
|
||||
if (isBlank(req.getName())) {
|
||||
errors.add(error("name", "name.required", "Register name is required"));
|
||||
} else if (req.getName().length() > 200) {
|
||||
errors.add(error("name", "name.exceeds.max.length", "Register name must be at most 200 characters"));
|
||||
}
|
||||
if (req.getFallbackNoCibilSlab() != null && req.getFallbackNoCibilSlab() < 1) {
|
||||
errors.add(error("fallbackNoCibilSlab", "fallback.no.cibil.slab.invalid", "Fallback slab pointer must be >= 1"));
|
||||
}
|
||||
if (req.getFallbackBelowMin() != null && !req.getFallbackBelowMin().equals("use-min-slab")
|
||||
&& !req.getFallbackBelowMin().equals("block")) {
|
||||
errors.add(error("fallbackBelowMin", "fallback.below.min.invalid",
|
||||
"fallbackBelowMin must be 'use-min-slab' or 'block'"));
|
||||
}
|
||||
|
||||
// Chart cells
|
||||
final List<SlabRequest> slabs = req.getSlabs();
|
||||
if (slabs == null || slabs.isEmpty()) {
|
||||
errors.add(error("slabs", "slabs.required", "At least one rate chart cell is required"));
|
||||
} else {
|
||||
for (int i = 0; i < slabs.size(); i++) {
|
||||
validateSlab(slabs.get(i), i, errors);
|
||||
}
|
||||
}
|
||||
|
||||
// CIBIL bands
|
||||
final List<CibilSlabRequest> cibil = req.getCibilSlabs();
|
||||
if (cibil == null || cibil.isEmpty()) {
|
||||
errors.add(error("cibilSlabs", "cibilSlabs.required", "At least one CIBIL slab is required"));
|
||||
} else {
|
||||
validateCibilCoverage(cibil, errors);
|
||||
}
|
||||
|
||||
raise(errors);
|
||||
}
|
||||
|
||||
private void validateSlab(final SlabRequest s, final int idx, final List<ApiParameterError> errors) {
|
||||
final String prefix = "slabs[" + idx + "]";
|
||||
if (s.getProductSrNo() == null) {
|
||||
errors.add(error(prefix + ".productSrNo", "productSrNo.required", "productSrNo is required"));
|
||||
}
|
||||
if (isBlank(s.getProductName())) {
|
||||
errors.add(error(prefix + ".productName", "productName.required", "productName is required"));
|
||||
}
|
||||
if (s.getSlabOrder() == null) {
|
||||
errors.add(error(prefix + ".slabOrder", "slabOrder.required", "slabOrder is required"));
|
||||
}
|
||||
if (isBlank(s.getSlabLabel())) {
|
||||
errors.add(error(prefix + ".slabLabel", "slabLabel.required", "slabLabel is required"));
|
||||
}
|
||||
// Exactly one of: rateValue populated, rateFormula populated, isNa=true. Both-null-and-not-NA is invalid.
|
||||
final boolean hasValue = s.getRateValue() != null;
|
||||
final boolean hasFormula = s.getRateFormula() != null && !s.getRateFormula().isBlank();
|
||||
final boolean na = Boolean.TRUE.equals(s.getIsNa());
|
||||
final int populated = (hasValue ? 1 : 0) + (hasFormula ? 1 : 0) + (na ? 1 : 0);
|
||||
if (populated == 0) {
|
||||
errors.add(error(prefix, "cell.empty",
|
||||
"Cell must have a rateValue, a rateFormula, or isNa=true"));
|
||||
} else if (populated > 1) {
|
||||
errors.add(error(prefix, "cell.ambiguous",
|
||||
"Cell must have exactly one of rateValue, rateFormula, or isNa"));
|
||||
}
|
||||
if (hasFormula && !Boolean.TRUE.equals(s.getIsFormula())) {
|
||||
errors.add(error(prefix + ".isFormula", "isFormula.mismatch",
|
||||
"isFormula must be true when rateFormula is set"));
|
||||
}
|
||||
}
|
||||
|
||||
/** CIBIL bands must cover 0..900 contiguously and must not overlap. */
|
||||
private void validateCibilCoverage(final List<CibilSlabRequest> cibil, final List<ApiParameterError> errors) {
|
||||
final List<CibilSlabRequest> sorted = new ArrayList<>(cibil);
|
||||
sorted.sort((a, b) -> Integer.compare(nz(a.getScoreFrom()), nz(b.getScoreFrom())));
|
||||
final Set<Integer> seenOrders = new TreeSet<>();
|
||||
int expectedNextFrom = 0;
|
||||
for (int i = 0; i < sorted.size(); i++) {
|
||||
final CibilSlabRequest c = sorted.get(i);
|
||||
final String prefix = "cibilSlabs[" + i + "]";
|
||||
if (c.getScoreFrom() == null || c.getScoreTo() == null) {
|
||||
errors.add(error(prefix, "cibil.range.required", "scoreFrom and scoreTo are required"));
|
||||
continue;
|
||||
}
|
||||
if (c.getScoreFrom() > c.getScoreTo()) {
|
||||
errors.add(error(prefix, "cibil.range.inverted", "scoreFrom must be <= scoreTo"));
|
||||
}
|
||||
if (c.getScoreFrom() != expectedNextFrom) {
|
||||
errors.add(error(prefix, "cibil.range.gap_or_overlap",
|
||||
"CIBIL bands must be contiguous starting from 0 (expected " + expectedNextFrom
|
||||
+ ", got " + c.getScoreFrom() + ")"));
|
||||
}
|
||||
if (isBlank(c.getTier())) {
|
||||
errors.add(error(prefix + ".tier", "tier.required", "tier is required"));
|
||||
}
|
||||
if (c.getAdjustmentPct() == null) {
|
||||
errors.add(error(prefix + ".adjustmentPct", "adjustmentPct.required", "adjustmentPct is required"));
|
||||
}
|
||||
if (c.getDisplayOrder() == null || !seenOrders.add(c.getDisplayOrder())) {
|
||||
errors.add(error(prefix + ".displayOrder", "displayOrder.invalid",
|
||||
"displayOrder is required and must be unique"));
|
||||
}
|
||||
expectedNextFrom = c.getScoreTo() + 1;
|
||||
}
|
||||
if (!sorted.isEmpty() && expectedNextFrom <= 900) {
|
||||
errors.add(error("cibilSlabs", "cibil.coverage.incomplete",
|
||||
"CIBIL bands must cover up to 900 (last to was " + (expectedNextFrom - 1) + ")"));
|
||||
}
|
||||
}
|
||||
|
||||
private static ApiParameterError error(final String field, final String code, final String msg) {
|
||||
return ApiParameterError.parameterError("validation." + RESOURCE_NAME + "." + code, msg, field);
|
||||
}
|
||||
|
||||
private static void raise(final List<ApiParameterError> errors) {
|
||||
if (!errors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException("validation.msg." + RESOURCE_NAME + ".failed",
|
||||
"Validation errors exist.", errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(final String s) {
|
||||
return s == null || s.isBlank();
|
||||
}
|
||||
|
||||
private static int nz(final Integer n) {
|
||||
return n != null ? n : 0;
|
||||
}
|
||||
}
|
||||
+373
@@ -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<String, Object> saveOrUpdate(final LoanProduct loanProduct, final JsonCommand command) {
|
||||
final Map<String, Object> 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<String, Object> 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<String, Object> 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> {
|
||||
|
||||
T get();
|
||||
}
|
||||
|
||||
private interface Ser<T> {
|
||||
|
||||
void set(T value);
|
||||
}
|
||||
|
||||
private <T> void putIfChanged(final String param, final T oldValue, final T newValue, final Ser<T> setter,
|
||||
final Map<String, Object> changes) {
|
||||
if (differs(oldValue, newValue)) {
|
||||
setter.set(newValue);
|
||||
changes.put(param, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void setString(final JsonCommand command, final String param, final Getter<String> getter,
|
||||
final Ser<String> setter, final Map<String, Object> 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<Long> getter,
|
||||
final Ser<Long> setter, final Map<String, Object> 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<Integer> getter,
|
||||
final Ser<Integer> setter, final Map<String, Object> 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<BigDecimal> getter,
|
||||
final Ser<BigDecimal> setter, final Map<String, Object> 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<Boolean> getter,
|
||||
final Ser<Boolean> setter, final Map<String, Object> 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<String, Object> toMap(final LoanProductAdvanceExt ext) {
|
||||
final Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
+11
@@ -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) //
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterData;
|
||||
|
||||
public interface LoanRateRegisterReadPlatformService {
|
||||
|
||||
/** All versions, newest-effective first — the Version History accordion feeds off this. */
|
||||
List<LoanRateRegisterData> retrieveAll();
|
||||
|
||||
/** The current active version, assembled with its chart cells + CIBIL bands. */
|
||||
Optional<LoanRateRegisterData> retrieveActive();
|
||||
|
||||
/** A specific version by id. */
|
||||
Optional<LoanRateRegisterData> retrieveById(Long id);
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterCibilSlabData;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterData;
|
||||
import org.apache.fineract.portfolio.loanproduct.data.LoanRateRegisterSlabData;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlab;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlabRepository;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffective;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffectiveRepository;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistory;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistoryRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Assembles {@link LoanRateRegisterData} responses by joining the three physical tables. Kept
|
||||
* read-only — all writes go through {@link LoanRateRegisterWritePlatformService}.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoanRateRegisterReadPlatformServiceImpl implements LoanRateRegisterReadPlatformService {
|
||||
|
||||
private final LoanRateRegisterEffectiveRepository effectiveRepository;
|
||||
private final LoanRateRegisterSlabHistoryRepository slabRepository;
|
||||
private final LoanRateRegisterCibilSlabRepository cibilRepository;
|
||||
|
||||
@Override
|
||||
public List<LoanRateRegisterData> retrieveAll() {
|
||||
final List<LoanRateRegisterEffective> versions = this.effectiveRepository.findAllByOrderByEffectiveFromDateDesc();
|
||||
final List<LoanRateRegisterData> out = new ArrayList<>(versions.size());
|
||||
for (final LoanRateRegisterEffective v : versions) {
|
||||
out.add(assemble(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<LoanRateRegisterData> retrieveActive() {
|
||||
return this.effectiveRepository.findFirstByIsActiveTrueOrderByEffectiveFromDateDesc().map(this::assemble);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<LoanRateRegisterData> retrieveById(final Long id) {
|
||||
return this.effectiveRepository.findById(id).map(this::assemble);
|
||||
}
|
||||
|
||||
/** Build the full response for one effective version — header + its chart cells + its CIBIL bands. */
|
||||
private LoanRateRegisterData assemble(final LoanRateRegisterEffective v) {
|
||||
final List<LoanRateRegisterSlabHistory> cells = this.slabRepository
|
||||
.findByEffectiveIdOrderByProductSrNoAscSlabOrderAsc(v.getId());
|
||||
final List<LoanRateRegisterCibilSlab> bands = this.cibilRepository
|
||||
.findByEffectiveIdOrderByDisplayOrderAsc(v.getId());
|
||||
|
||||
final List<LoanRateRegisterSlabData> slabData = new ArrayList<>(cells.size());
|
||||
for (final LoanRateRegisterSlabHistory c : cells) {
|
||||
slabData.add(LoanRateRegisterSlabData.builder()
|
||||
.id(c.getId())
|
||||
.productSrNo(c.getProductSrNo())
|
||||
.productName(c.getProductName())
|
||||
.slabOrder(c.getSlabOrder())
|
||||
.slabLabel(c.getSlabLabel())
|
||||
.amountUpto(c.getAmountUpto())
|
||||
.rateValue(c.getRateValue())
|
||||
.rateFormula(c.getRateFormula())
|
||||
.isFormula(c.getIsFormula())
|
||||
.isNa(c.getIsNa())
|
||||
.build());
|
||||
}
|
||||
|
||||
final List<LoanRateRegisterCibilSlabData> cibilData = new ArrayList<>(bands.size());
|
||||
for (final LoanRateRegisterCibilSlab b : bands) {
|
||||
cibilData.add(LoanRateRegisterCibilSlabData.builder()
|
||||
.id(b.getId())
|
||||
.scoreFrom(b.getScoreFrom())
|
||||
.scoreTo(b.getScoreTo())
|
||||
.tier(b.getTier())
|
||||
.slabType(b.getSlabType())
|
||||
.adjustmentPct(b.getAdjustmentPct())
|
||||
.displayOrder(b.getDisplayOrder())
|
||||
.build());
|
||||
}
|
||||
|
||||
return LoanRateRegisterData.builder()
|
||||
.id(v.getId())
|
||||
.name(v.getName())
|
||||
.effectiveFromDate(v.getEffectiveFromDate())
|
||||
.effectiveToDate(v.getEffectiveToDate())
|
||||
.currencyCode(v.getCurrencyCode())
|
||||
.isActive(v.getIsActive())
|
||||
.fallbackNoCibilSlab(v.getFallbackNoCibilSlab())
|
||||
.fallbackBelowMin(v.getFallbackBelowMin())
|
||||
.note(v.getNote())
|
||||
.createdById(v.getCreatedById())
|
||||
.createdDate(v.getCreatedDate())
|
||||
.slabs(slabData)
|
||||
.cibilSlabs(cibilData)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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 org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
|
||||
public interface LoanRateRegisterWritePlatformService {
|
||||
|
||||
/** Save a new version of the loan interest rate register (maker-checker gated upstream). */
|
||||
CommandProcessingResult saveRateRegister(JsonCommand command);
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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.Gson;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest;
|
||||
import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.CibilSlabRequest;
|
||||
import org.apache.fineract.portfolio.loanproduct.api.LoanRateRegisterSaveRequest.SlabRequest;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlab;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterCibilSlabRepository;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffective;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterEffectiveRepository;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistory;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanRateRegisterSlabHistoryRepository;
|
||||
import org.apache.fineract.portfolio.loanproduct.serialization.LoanRateRegisterDataValidator;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Persists a new effective version of the Loan Interest Rate Register. Called from the command
|
||||
* handler, which Fineract routes to only AFTER a checker has approved the maker's submission. The
|
||||
* whole save runs inside one transaction: previous active deactivated → new effective inserted →
|
||||
* all chart cells + CIBIL bands inserted. Any failure rolls everything back.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoanRateRegisterWritePlatformServiceImpl implements LoanRateRegisterWritePlatformService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final LoanRateRegisterDataValidator validator;
|
||||
private final LoanRateRegisterEffectiveRepository effectiveRepository;
|
||||
private final LoanRateRegisterSlabHistoryRepository slabRepository;
|
||||
private final LoanRateRegisterCibilSlabRepository cibilRepository;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult saveRateRegister(final JsonCommand command) {
|
||||
final LoanRateRegisterSaveRequest req = gson.fromJson(command.json(), LoanRateRegisterSaveRequest.class);
|
||||
this.validator.validate(req);
|
||||
|
||||
final Long createdById = this.context.authenticatedUser().getId();
|
||||
final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
|
||||
final LocalDate effectiveFrom = parseDateOrToday(req.getEffectiveFromDate(), today);
|
||||
|
||||
// 1) Deactivate the currently-active version (if any).
|
||||
final List<LoanRateRegisterEffective> active = this.effectiveRepository.findByIsActiveTrueOrderByEffectiveFromDateDesc();
|
||||
for (final LoanRateRegisterEffective prior : active) {
|
||||
prior.deactivate(today);
|
||||
}
|
||||
if (!active.isEmpty()) {
|
||||
this.effectiveRepository.saveAll(active);
|
||||
}
|
||||
|
||||
// 2) Insert the new effective row.
|
||||
final LoanRateRegisterEffective fresh = LoanRateRegisterEffective.create(req.getName(), effectiveFrom,
|
||||
req.getCurrencyCode(), req.getFallbackNoCibilSlab(), req.getFallbackBelowMin(), req.getNote(), createdById);
|
||||
final LoanRateRegisterEffective saved = this.effectiveRepository.save(fresh);
|
||||
final Long versionId = saved.getId();
|
||||
|
||||
// 3) Insert all chart cells for this version.
|
||||
final List<LoanRateRegisterSlabHistory> cells = new ArrayList<>(req.getSlabs().size());
|
||||
for (final SlabRequest s : req.getSlabs()) {
|
||||
cells.add(LoanRateRegisterSlabHistory.create(versionId, s.getProductSrNo(), s.getProductName(), s.getSlabOrder(),
|
||||
s.getSlabLabel(), s.getAmountUpto(), s.getRateValue(), s.getRateFormula(),
|
||||
Boolean.TRUE.equals(s.getIsFormula()), Boolean.TRUE.equals(s.getIsNa())));
|
||||
}
|
||||
this.slabRepository.saveAll(cells);
|
||||
|
||||
// 4) Insert all CIBIL bands for this version.
|
||||
final List<LoanRateRegisterCibilSlab> bands = new ArrayList<>(req.getCibilSlabs().size());
|
||||
for (final CibilSlabRequest c : req.getCibilSlabs()) {
|
||||
bands.add(LoanRateRegisterCibilSlab.create(versionId, c.getScoreFrom(), c.getScoreTo(), c.getTier(),
|
||||
c.getSlabType(), c.getAdjustmentPct(), c.getDisplayOrder()));
|
||||
}
|
||||
this.cibilRepository.saveAll(bands);
|
||||
|
||||
return new CommandProcessingResultBuilder()
|
||||
.withCommandId(command.commandId())
|
||||
.withEntityId(versionId)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static LocalDate parseDateOrToday(final String iso, final LocalDate today) {
|
||||
if (iso == null || iso.isBlank()) {
|
||||
return today;
|
||||
}
|
||||
return LocalDate.parse(iso, DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ fineract.security.oauth2.client.registrations.frontend-client.require-authorizat
|
||||
fineract.tenant.host=${FINERACT_DEFAULT_TENANTDB_HOSTNAME:localhost}
|
||||
fineract.tenant.port=${FINERACT_DEFAULT_TENANTDB_PORT:5432}
|
||||
fineract.tenant.username=${FINERACT_DEFAULT_TENANTDB_UID:postgres}
|
||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:Keshav@898966}
|
||||
fineract.tenant.password=${FINERACT_DEFAULT_TENANTDB_PWD:UAT@DB_2025}
|
||||
fineract.tenant.parameters=${FINERACT_DEFAULT_TENANTDB_CONN_PARAMS:}
|
||||
fineract.tenant.timezone=${FINERACT_DEFAULT_TENANTDB_TIMEZONE:Asia/Kolkata}
|
||||
fineract.tenant.identifier=${FINERACT_DEFAULT_TENANTDB_IDENTIFIER:default}
|
||||
@@ -170,6 +170,7 @@ fineract.loan.transactionprocessor.mifos-standard.enabled=${FINERACT_LOAN_TRANSA
|
||||
fineract.loan.transactionprocessor.heavensfamily.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_HEAVENSFAMILY_ENABLED:true}
|
||||
fineract.loan.transactionprocessor.interest-principal-penalties-fees.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_INTEREST_PRINCIPAL_PENALTIES_FEES_ENABLED:true}
|
||||
fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_PRINCIPAL_INTEREST_PENALTIES_FEES_ENABLED:true}
|
||||
fineract.loan.transactionprocessor.fees-penalties-interest-principal.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_FEES_PENALTIES_INTEREST_PRINCIPAL_ENABLED:true}
|
||||
fineract.loan.transactionprocessor.rbi-india.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_RBI_INDIA_ENABLED:true}
|
||||
fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_ENABLED:true}
|
||||
fineract.loan.transactionprocessor.due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_ENABLED:true}
|
||||
@@ -415,7 +416,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL
|
||||
spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver}
|
||||
spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants}
|
||||
spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres}
|
||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:Keshav@898966}
|
||||
spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:UAT@DB_2025}
|
||||
spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3}
|
||||
spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10}
|
||||
spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000}
|
||||
|
||||
@@ -615,4 +615,8 @@
|
||||
<include file="parts/0539_map_profit_financial_activities.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0544_dividend_round_off_financial_activity.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0545_ensure_profit_financial_activities_mapped.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0571_create_loan_product_advance_ext.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0572_add_loan_product_advance_ext_fields.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0580_create_loan_rate_register_tables.xml" relativeToChangelogFile="true"/>
|
||||
<include file="parts/0581_enable_loan_rate_register_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!-- Satellite table for "Advances A/C" loan-product settings that Fineract's m_product_loan has no
|
||||
home for (security & margin, lien, deposit-linked interest, penal/NPA, renewal, monitoring,
|
||||
cheque book). Keyed 1:1 by loan_product_id; standalone so the LoanProduct entity graph is
|
||||
untouched. Only Advances products get a row. -->
|
||||
<changeSet author="cbsnexora" id="0571_create_loan_product_advance_ext">
|
||||
<createTable tableName="m_product_loan_advance_ext">
|
||||
<column name="loan_product_id" type="BIGINT">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<!-- classification -->
|
||||
<column name="product_type_group" type="VARCHAR(50)"/>
|
||||
<column name="product_type_sub_group" type="VARCHAR(50)"/>
|
||||
<column name="gl_account_id" type="BIGINT"/>
|
||||
<column name="loan_product_category" type="VARCHAR(50)"/>
|
||||
<column name="priority_sector" type="VARCHAR(50)"/>
|
||||
<column name="cibil_account_type" type="VARCHAR(50)"/>
|
||||
<column name="kfs_enabled" type="BOOLEAN"/>
|
||||
<column name="apr_enabled" type="BOOLEAN"/>
|
||||
<!-- facility -->
|
||||
<column name="facility_type" type="VARCHAR(30)"/>
|
||||
<!-- security & margin -->
|
||||
<column name="security_type" type="VARCHAR(50)"/>
|
||||
<column name="charge_type" type="VARCHAR(30)"/>
|
||||
<column name="eligible_amount_basis" type="VARCHAR(50)"/>
|
||||
<column name="margin_pct" type="DECIMAL(9,4)"/>
|
||||
<column name="ltv_pct" type="DECIMAL(9,4)"/>
|
||||
<column name="dp_method" type="VARCHAR(50)"/>
|
||||
<!-- lien -->
|
||||
<column name="lien_required" type="BOOLEAN"/>
|
||||
<column name="lien_marking_stage" type="VARCHAR(30)"/>
|
||||
<column name="lien_certificate_required" type="BOOLEAN"/>
|
||||
<column name="lien_certificate_prefix" type="VARCHAR(20)"/>
|
||||
<column name="print_certificate_on_disbursement" type="BOOLEAN"/>
|
||||
<!-- interest extras -->
|
||||
<column name="interest_basis" type="VARCHAR(30)"/>
|
||||
<column name="spread_pct" type="DECIMAL(9,4)"/>
|
||||
<column name="interest_charged_on" type="VARCHAR(30)"/>
|
||||
<column name="interest_posting_frequency" type="VARCHAR(30)"/>
|
||||
<!-- penal & NPA extras -->
|
||||
<column name="penal_applicable" type="BOOLEAN"/>
|
||||
<column name="penal_rate_pct" type="DECIMAL(9,4)"/>
|
||||
<column name="penal_applied_on" type="VARCHAR(30)"/>
|
||||
<column name="penal_grace_days" type="INT"/>
|
||||
<column name="npa_trigger" type="VARCHAR(30)"/>
|
||||
<column name="interest_after_npa" type="VARCHAR(30)"/>
|
||||
<column name="undebited_interest_gl_id" type="BIGINT"/>
|
||||
<!-- renewal & closure -->
|
||||
<column name="renewal_allowed" type="BOOLEAN"/>
|
||||
<column name="renewal_period_months" type="INT"/>
|
||||
<column name="renewal_grace_days" type="INT"/>
|
||||
<column name="preclosure_allowed" type="VARCHAR(30)"/>
|
||||
<column name="lock_in_months" type="INT"/>
|
||||
<column name="interest_on_preclosure" type="VARCHAR(30)"/>
|
||||
<!-- monitoring -->
|
||||
<column name="revaluation_freq_months" type="INT"/>
|
||||
<column name="insurance_required" type="BOOLEAN"/>
|
||||
<column name="stock_statement_freq_months" type="INT"/>
|
||||
<!-- cheque book -->
|
||||
<column name="cheque_book_enabled" type="BOOLEAN"/>
|
||||
<column name="cheque_type" type="VARCHAR(50)"/>
|
||||
<column name="default_cheque_book_size" type="INT"/>
|
||||
<column name="pps_threshold" type="DECIMAL(19,6)"/>
|
||||
<column name="pps_mandatory_above" type="DECIMAL(19,6)"/>
|
||||
<!-- share (co-op) -->
|
||||
<column name="share_capital_gl_id" type="BIGINT"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="cbsnexora" id="0571_loan_product_advance_ext_product_fk">
|
||||
<addForeignKeyConstraint baseTableName="m_product_loan_advance_ext" baseColumnNames="loan_product_id"
|
||||
constraintName="fk_mplae_loan_product" referencedTableName="m_product_loan"
|
||||
referencedColumnNames="id" onDelete="CASCADE"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="cbsnexora" id="0571_loan_product_advance_ext_gl_fks">
|
||||
<addForeignKeyConstraint baseTableName="m_product_loan_advance_ext" baseColumnNames="gl_account_id"
|
||||
constraintName="fk_mplae_gl_account" referencedTableName="acc_gl_account"
|
||||
referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_product_loan_advance_ext" baseColumnNames="undebited_interest_gl_id"
|
||||
constraintName="fk_mplae_undebited_gl" referencedTableName="acc_gl_account"
|
||||
referencedColumnNames="id"/>
|
||||
<addForeignKeyConstraint baseTableName="m_product_loan_advance_ext" baseColumnNames="share_capital_gl_id"
|
||||
constraintName="fk_mplae_share_capital_gl" referencedTableName="acc_gl_account"
|
||||
referencedColumnNames="id"/>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!-- Remaining "Advances A/C" wizard fields so every field the officer fills is persisted:
|
||||
lien rules, eligible deposit products, repayment/appropriation, account behaviour,
|
||||
interest extras, NPA upgrade rules, and the full cheque-book configuration. -->
|
||||
<changeSet author="cbsnexora" id="0572_add_loan_product_advance_ext_fields">
|
||||
<addColumn tableName="m_product_loan_advance_ext">
|
||||
<!-- security & lien extras -->
|
||||
<column name="eligible_deposit_products" type="VARCHAR(500)"/>
|
||||
<column name="lien_tenure_not_exceed_maturity" type="BOOLEAN"/>
|
||||
<column name="lien_auto_release_on_closure" type="BOOLEAN"/>
|
||||
<column name="lien_allow_multiple_deposits" type="BOOLEAN"/>
|
||||
<column name="lien_adjust_from_maturity" type="BOOLEAN"/>
|
||||
<column name="lien_allow_third_party" type="BOOLEAN"/>
|
||||
<column name="lien_allow_renewal_under_lien" type="BOOLEAN"/>
|
||||
<!-- limit & tenure extras -->
|
||||
<column name="repayment_type" type="VARCHAR(40)"/>
|
||||
<column name="appropriation_order" type="VARCHAR(60)"/>
|
||||
<column name="debit_balance_allow" type="BOOLEAN"/>
|
||||
<column name="allow_debit_after_due_date" type="BOOLEAN"/>
|
||||
<column name="branch_level_account_type" type="BOOLEAN"/>
|
||||
<!-- interest extras -->
|
||||
<column name="interest_calc_method" type="VARCHAR(30)"/>
|
||||
<column name="interest_on_credit_balance" type="BOOLEAN"/>
|
||||
<column name="interest_posting_day" type="VARCHAR(30)"/>
|
||||
<!-- NPA upgrade rules -->
|
||||
<column name="npa_upgrade_only_all_arrears" type="BOOLEAN"/>
|
||||
<column name="npa_keep_standard_if_deposit_covers" type="BOOLEAN"/>
|
||||
<!-- cheque book full config -->
|
||||
<column name="cheque_book_sizes" type="VARCHAR(50)"/>
|
||||
<column name="cheque_max_books_outstanding" type="INT"/>
|
||||
<column name="cheque_reissue_when_leaves" type="INT"/>
|
||||
<column name="cheque_first_book_free" type="BOOLEAN"/>
|
||||
<column name="cheque_free_books_per_year" type="INT"/>
|
||||
<column name="cheque_name_printed" type="VARCHAR(50)"/>
|
||||
<column name="cheque_print_description" type="VARCHAR(100)"/>
|
||||
<column name="cheque_stop_payment_allowed" type="BOOLEAN"/>
|
||||
<column name="cheque_maintain_stock_register" type="BOOLEAN"/>
|
||||
<column name="cheque_allow_withdrawal_slip" type="BOOLEAN"/>
|
||||
<column name="cheque_block_npa" type="BOOLEAN"/>
|
||||
<column name="cheque_block_limit_expired" type="BOOLEAN"/>
|
||||
<column name="cheque_block_over_dp" type="BOOLEAN"/>
|
||||
<column name="cheque_block_frozen" type="BOOLEAN"/>
|
||||
<column name="cheque_block_lien_released" type="BOOLEAN"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Central Loan Interest Rate Register — one approved rate chart (34 products × 10 amount
|
||||
slabs) + CIBIL/credit-score adjustment bands. Mirrors the "CBS Base Interest Rate" (FD)
|
||||
versioning model: each save creates a new effective row and deactivates the previous one.
|
||||
Maker-checker gated through Fineract's standard command-source mechanism (see 0581).
|
||||
-->
|
||||
|
||||
<!-- 1. Version header: one row per effective version. is_active flags the currently in-force one. -->
|
||||
<changeSet id="0580a_create_loan_rate_register_effective" author="cbsnexora">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_loan_rate_register_effective"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_loan_rate_register_effective">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="name" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="effective_from_date" type="DATE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="effective_to_date" type="DATE"/>
|
||||
<column name="currency_code" type="VARCHAR(3)" defaultValue="INR">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="is_active" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Fallback behaviour when a loan's CIBIL score is unavailable: pointer into the CIBIL
|
||||
slab list for this version (1-indexed, matches UI). -->
|
||||
<column name="fallback_no_cibil_slab" type="INT" defaultValueNumeric="2">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Fallback when CIBIL < min configured slab: 'use-min-slab' or 'block'. -->
|
||||
<column name="fallback_below_min" type="VARCHAR(20)" defaultValue="block">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="note" type="VARCHAR(500)"/>
|
||||
<column name="created_by_id" type="BIGINT"/>
|
||||
<column name="created_date" type="DATETIME"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- 2. Rate chart cells: one row per {version × product × amount-slab}. Product name and slab
|
||||
label are denormalised for query convenience + display. Cell value is either a plain rate,
|
||||
a formula string ("1% + FD"), or N.A. (both value and formula null, is_na=true). -->
|
||||
<changeSet id="0580b_create_loan_rate_register_slab_history" author="cbsnexora">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_loan_rate_register_slab_history"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_loan_rate_register_slab_history">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="effective_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Product dimension -->
|
||||
<column name="product_sr_no" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="product_name" type="VARCHAR(200)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Amount-slab dimension -->
|
||||
<column name="slab_order" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="slab_label" type="VARCHAR(60)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="amount_upto" type="DECIMAL(19,2)"/>
|
||||
<!-- Cell value: at most ONE of rate_value / rate_formula is non-null; both null + is_na=true means slab not offered. -->
|
||||
<column name="rate_value" type="DECIMAL(9,4)"/>
|
||||
<column name="rate_formula" type="VARCHAR(50)"/>
|
||||
<column name="is_formula" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="is_na" type="BOOLEAN" defaultValueBoolean="false">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="DATETIME"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- 3. CIBIL/credit-score adjustment bands: 5 rows per version (typical), layered on top of the
|
||||
chart rate at loan sanction. -->
|
||||
<changeSet id="0580c_create_loan_rate_register_cibil_slab" author="cbsnexora">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<preConditions onFail="MARK_RAN">
|
||||
<not><tableExists tableName="m_loan_rate_register_cibil_slab"/></not>
|
||||
</preConditions>
|
||||
<createTable tableName="m_loan_rate_register_cibil_slab">
|
||||
<column name="id" type="BIGINT" autoIncrement="true">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="effective_id" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="score_from" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="score_to" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="tier" type="VARCHAR(30)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="slab_type" type="VARCHAR(20)" defaultValue="Flat">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="adjustment_pct" type="DECIMAL(5,2)" defaultValueNumeric="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="display_order" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_date" type="DATETIME"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
|
||||
<!-- 4. Foreign keys — effective_id in child tables cascades on delete so retiring a version
|
||||
cleans up its slabs and CIBIL rows atomically. -->
|
||||
<changeSet id="0580d_loan_rate_register_fks" author="cbsnexora">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<addForeignKeyConstraint baseTableName="m_loan_rate_register_slab_history"
|
||||
baseColumnNames="effective_id"
|
||||
constraintName="fk_mlrrsh_effective"
|
||||
referencedTableName="m_loan_rate_register_effective"
|
||||
referencedColumnNames="id"
|
||||
onDelete="CASCADE"/>
|
||||
<addForeignKeyConstraint baseTableName="m_loan_rate_register_cibil_slab"
|
||||
baseColumnNames="effective_id"
|
||||
constraintName="fk_mlrrcs_effective"
|
||||
referencedTableName="m_loan_rate_register_effective"
|
||||
referencedColumnNames="id"
|
||||
onDelete="CASCADE"/>
|
||||
<addForeignKeyConstraint baseTableName="m_loan_rate_register_effective"
|
||||
baseColumnNames="created_by_id"
|
||||
constraintName="fk_mlrre_created_by"
|
||||
referencedTableName="m_appuser"
|
||||
referencedColumnNames="id"/>
|
||||
</changeSet>
|
||||
|
||||
<!-- 5. Indexes for the hot read paths: (a) "give me the current active" (b) "give me a version's
|
||||
rate for a product+slab" (c) "give me a version's CIBIL slabs ordered". -->
|
||||
<changeSet id="0580e_loan_rate_register_indexes" author="cbsnexora">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<createIndex indexName="idx_mlrre_active" tableName="m_loan_rate_register_effective">
|
||||
<column name="is_active"/>
|
||||
<column name="effective_from_date"/>
|
||||
</createIndex>
|
||||
<createIndex indexName="idx_mlrrsh_lookup" tableName="m_loan_rate_register_slab_history">
|
||||
<column name="effective_id"/>
|
||||
<column name="product_sr_no"/>
|
||||
<column name="slab_order"/>
|
||||
</createIndex>
|
||||
<createIndex indexName="idx_mlrrcs_order" tableName="m_loan_rate_register_cibil_slab">
|
||||
<column name="effective_id"/>
|
||||
<column name="display_order"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
|
||||
<!--
|
||||
Enable maker-checker on the Loan Interest Rate Register. Mirrors 0470 for the FD side.
|
||||
Two permissions: the maker's gated with can_maker_checker=true so submits land in
|
||||
m_portfolio_command_source as PENDING; the checker approves from the standard Checker Inbox
|
||||
via /makercheckers/{id}?command=approve, which fires CreateCbsLoanBaseInterestRateCommandHandler.
|
||||
-->
|
||||
|
||||
<changeSet id="0581a_loan_rate_register_maker_checker_permissions" author="cbsnexora" runOnChange="false">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<sql>
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'CREATE_CBSLOANBASEINTERESTRATE', 'CBSLOANBASEINTERESTRATE', 'CREATE', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE');
|
||||
|
||||
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||
SELECT 'portfolio', 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER', 'CBSLOANBASEINTERESTRATE', 'CREATE_CHECKER', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER');
|
||||
|
||||
UPDATE m_permission
|
||||
SET can_maker_checker = TRUE
|
||||
WHERE entity_name = 'CBSLOANBASEINTERESTRATE'
|
||||
AND action_name = 'CREATE';
|
||||
|
||||
UPDATE m_permission
|
||||
SET action_name = 'CREATE_CHECKER'
|
||||
WHERE entity_name = 'CBSLOANBASEINTERESTRATE'
|
||||
AND code = 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER';
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
<changeSet id="0581b_grant_loan_rate_register_permissions_to_roles" author="cbsnexora" runOnChange="false">
|
||||
<validCheckSum>ANY</validCheckSum>
|
||||
<sql>
|
||||
-- Super user: both maker and checker
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE r.name = 'Super user'
|
||||
AND p.code IN ('CREATE_CBSLOANBASEINTERESTRATE', 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
-- bankadmin user's role(s): both maker and checker
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT ur.role_id, p.id
|
||||
FROM m_appuser u
|
||||
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||
CROSS JOIN m_permission p
|
||||
WHERE u.username = 'bankadmin'
|
||||
AND p.code IN ('CREATE_CBSLOANBASEINTERESTRATE', 'CREATE_CBSLOANBASEINTERESTRATE_CHECKER')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
-- Clerk: maker only (separation of duties — clerks submit, higher authority approves)
|
||||
INSERT INTO m_role_permission (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM m_role r
|
||||
CROSS JOIN m_permission p
|
||||
WHERE LOWER(r.name) = 'clerk'
|
||||
AND p.code = 'CREATE_CBSLOANBASEINTERESTRATE'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM m_role_permission rp
|
||||
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
);
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -87,6 +87,7 @@ fineract.loan.transactionprocessor.mifos-standard.enabled=true
|
||||
fineract.loan.transactionprocessor.heavensfamily.enabled=true
|
||||
fineract.loan.transactionprocessor.interest-principal-penalties-fees.enabled=true
|
||||
fineract.loan.transactionprocessor.principal-interest-penalties-fees.enabled=true
|
||||
fineract.loan.transactionprocessor.fees-penalties-interest-principal.enabled=true
|
||||
fineract.loan.transactionprocessor.rbi-india.enabled=true
|
||||
fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance-principal-penalty-fee-interest.enabled=true
|
||||
fineract.loan.transactionprocessor.due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee.enabled=true
|
||||
|
||||
Reference in New Issue
Block a user