Merge branch 'feature/advanceLoanProduct' into merge/dds-advance-loan-into-dev-latest
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Adds the Advance Loan Product backend on top of the DDS merge of
dev-latest:
- Advance loan product ext entity, API and Liquibase
(LoanProductAdvanceExtService; loan product API accepts and returns
"advanceExtensions")
- Fees-Penalty-Interest-Principal repayment transaction processor,
enabled by fineract.loan.transactionprocessor.
fees-penalties-interest-principal.enabled (default true)
- Loan interest rate register backend
Conflict resolved:
- changelog-tenant.xml: kept both. The four advance loan changelogs
(0571_create_loan_product_advance_ext, 0572_add_loan_product_advance_
ext_fields, 0580_create_loan_rate_register_tables,
0581_enable_loan_rate_register_maker_checker) go just before the DDS
block, which its comment says stays last. Their numbers overlap
dev-latest files but no file name is shared.
Not taken from feature/advanceLoanProduct:
- Commit b72657703 changed the default DB password in
application.properties to a UAT database password. That merged
without a conflict; the dev-latest password lines are kept so local
setups keep working and the UAT password does not go into the
repository. The new transaction processor property from the same
file is kept.
Nothing from dev-latest or DDS is removed: no file deleted; only
LoanProductsApiResource (one supported parameter added),
LoanProductConfiguration (one constructor argument added) and
application.properties change existing lines. CommandWrapperBuilder,
changed on both sides, has no duplicate methods.
Verified with :fineract-provider:compileJava.
Branch renamed from merge/dds-into-dev-latest, since it now carries
both DDS and Advance Loan Product.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+8
@@ -2460,6 +2460,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -615,6 +615,10 @@
|
||||
<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"/>
|
||||
|
||||
<!-- DDS: the scheme's own tables, settings and permissions. Kept together and last, because
|
||||
each one builds on the core the block above sets up and none of it builds on DDS. -->
|
||||
|
||||
+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