Compare commits

...
Author SHA1 Message Date
sonaliandClaude Sonnet 5.5 e95e981bcd feat(loan-products): Gold and Warehouse Receipt product fields with validation
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 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 - 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 Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (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-5) (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 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 - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
- Liquibase 0604 to 0607: columns on m_product_loan_advance_ext for Loan against Gold (LTV cap, purity,
  rate source, weights, appraiser rule, packet prefix, auction notice, revaluation on NPA and its charge,
  partial release, third-party gold, disbursement mode and cash limit, LTV breach alert, auction reserve
  price, redeem before auction) and for Warehouse Receipt (receipt type, WDRA and assay rules, tenure
  within receipt validity, repository pledge, commodity groups, valuation price source, max loan per
  quintal, stock inspection, LTV alert, partial release, disposal notice and reserve price).
- LoanProductAdvanceExt and LoanProductAdvanceExtService: the new fields are accepted, saved and read
  back (so Edit and View can use them).
- Validation on the merged state: the LTV cap must cover the LTV the margin gives, the breach alert may
  not sit below the cap, tenure is at most 12 months, redeem-before-sale cannot be switched off, a gold
  cash disbursement limit is at most 20,000, a warehouse needs a WDRA-registered warehouse and an assay
  certificate, and the receipt type, commodity groups and price source must be known values.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 10:55:48 +05:30
sonali 0191461102 Merge branch 'dev-latest' of https://git.lincpay.tech/CBSNexora/cbsnexora-backend into feature/advanceLoanProduct
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (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 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 Docker Builds / build (true, docker-compose.yml, mariadb) (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 - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (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 - PostgreSQL / test (test-core-1) (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 Messaging Smoke Tests / Smoke Test with Kafka (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-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
2026-10-16 10:36:17 +05:30
sonali 4c40784a0e implemented the Edit prodict functionality for the RD,FD,DDS,LIC 2026-10-16 10:33:57 +05:30
32 changed files with 1434 additions and 32 deletions
@@ -113,7 +113,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue())
.value(incomeFromRecoveryAccountId).notNull().integerGreaterThanZero();
.value(incomeFromRecoveryAccountId).ignoreIfNull().integerGreaterThanZero();
final Long writeOffAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
@@ -128,7 +128,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
.value(transfersInSuspenseAccountId).ignoreIfNull().integerGreaterThanZero();
}
@@ -46,17 +46,27 @@ public class DelinquencyRange extends AbstractAuditableWithUTCDateTimeCustom<Lon
@Column(name = "max_age_days", nullable = true)
private Integer maximumAgeDays;
/**
* Which ECL stage (1, 2 or 3) this range maps to under the RBI 2026 Directions. Nullable for ranges created
* before this column existed; a range with no stage is treated as informational only and does not drive
* staging or provisioning.
*/
@Column(name = "stage", nullable = true)
private Integer stage;
@Version
private Long version;
protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays) {
protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays, Integer stage) {
this.classification = classification;
this.minimumAgeDays = minimumAgeDays;
this.maximumAgeDays = maximumAgeDays;
this.stage = stage;
}
public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge) {
return new DelinquencyRange(classification, minimumAge, maximumAge);
public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge,
Integer stage) {
return new DelinquencyRange(classification, minimumAge, maximumAge, stage);
}
}
@@ -53,12 +53,20 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
paymentTypeRepositoryWrapper, codeValueRepository);
}
/** Only the GLs the request names are mapped; the bank maps nine and leaves the other placeholders unmapped. */
private boolean isMapped(final JsonElement element, final String paramName) {
return this.fromApiJsonHelper.extractLongNamed(paramName, element) != null;
}
/***
* Set of abstractions for saving Loan Products to GL Account Mappings
***/
public void saveLoanToAssetAccountMapping(final JsonElement element, final String paramName, final Long productId,
final int placeHolderTypeId) {
if (!isMapped(element, paramName)) {
return;
}
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.ASSET, PortfolioProductType.LOAN);
}
@@ -72,16 +80,25 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
public void saveLoanToIncomeAccountMapping(final JsonElement element, final String paramName, final Long productId,
final int placeHolderTypeId) {
if (!isMapped(element, paramName)) {
return;
}
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.INCOME, PortfolioProductType.LOAN);
}
public void saveLoanToExpenseAccountMapping(final JsonElement element, final String paramName, final Long productId,
final int placeHolderTypeId) {
if (!isMapped(element, paramName)) {
return;
}
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.EXPENSE, PortfolioProductType.LOAN);
}
public void saveLoanToLiabilityAccountMapping(final JsonElement element, final String paramName, final Long productId,
final int placeHolderTypeId) {
if (!isMapped(element, paramName)) {
return;
}
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.LIABILITY, PortfolioProductType.LOAN);
}
@@ -29,6 +29,7 @@ public final class DelinquencyApiConstants {
public static final String RANGES_PARAM_NAME = "ranges";
public static final String MINIMUMAGEDAYS_PARAM_NAME = "minimumAgeDays";
public static final String MAXIMUMAGEDAYS_PARAM_NAME = "maximumAgeDays";
public static final String STAGE_PARAM_NAME = "stage";
public static final String LOCALE_PARAM_NAME = "locale";
}
@@ -35,5 +35,6 @@ public class DelinquencyRangeRequest implements Serializable {
private String classification;
private Integer minimumAgeDays;
private Integer maximumAgeDays;
private Integer stage;
private String locale;
}
@@ -34,13 +34,15 @@ public class DelinquencyRangeData implements Serializable {
private String classification;
private Integer minimumAgeDays;
private Integer maximumAgeDays;
/** ECL stage (1, 2 or 3) this range maps to under the RBI 2026 Directions; null on ranges predating this field. */
private Integer stage;
public static DelinquencyRangeData instance(String classification, Integer minimumAgeDays, Integer maximumAgeDays) {
return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays);
public static DelinquencyRangeData instance(String classification, Integer minimumAgeDays, Integer maximumAgeDays, Integer stage) {
return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays, stage);
}
public static DelinquencyRangeData reference(Long id) {
return new DelinquencyRangeData(id, "", 0, 0);
return new DelinquencyRangeData(id, "", 0, 0, null);
}
}
@@ -305,7 +305,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays());
}
DelinquencyRange newDelinquencyRange = DelinquencyRange.instance(data.getClassification(), data.getMinimumAgeDays(),
data.getMaximumAgeDays());
data.getMaximumAgeDays(), data.getStage());
return repositoryRange.saveAndFlush(newDelinquencyRange);
} else {
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated",
@@ -327,6 +327,10 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
delinquencyRange.setMaximumAgeDays(data.getMaximumAgeDays());
changes.put(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, data.getMaximumAgeDays());
}
if (!java.util.Objects.equals(data.getStage(), delinquencyRange.getStage())) {
delinquencyRange.setStage(data.getStage());
changes.put(DelinquencyApiConstants.STAGE_PARAM_NAME, data.getStage());
}
if (!changes.isEmpty()) {
delinquencyRange = repositoryRange.saveAndFlush(delinquencyRange);
}
@@ -56,7 +56,8 @@ public class DelinquencyRangeParseAndValidator extends ParseAndValidator {
jsonHelper.checkForUnsupportedParameters(element,
List.of(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME,
DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.LOCALE_PARAM_NAME));
DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.STAGE_PARAM_NAME,
DelinquencyApiConstants.LOCALE_PARAM_NAME));
final String localeValue = jsonHelper.extractStringNamed(DelinquencyApiConstants.LOCALE_PARAM_NAME, element);
dataValidator.reset().parameter(DelinquencyApiConstants.LOCALE_PARAM_NAME).value(localeValue).notBlank();
@@ -65,13 +66,15 @@ public class DelinquencyRangeParseAndValidator extends ParseAndValidator {
final String classification = jsonHelper.extractStringNamed(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, element);
final Integer minimumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, element, locale);
final Integer maximumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, element, locale);
final Integer stage = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.STAGE_PARAM_NAME, element, locale);
dataValidator.reset().parameter(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME).value(classification).notBlank();
dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME).value(minimumAge).notBlank()
.integerGreaterThanNumber(0);
dataValidator.reset().parameter(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME).value(maximumAge).ignoreIfNull()
.integerGreaterThanNumber(0);
dataValidator.reset().parameter(DelinquencyApiConstants.STAGE_PARAM_NAME).value(stage).ignoreIfNull().inMinMaxRange(1, 3);
return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge);
return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge, stage);
}
}
@@ -137,6 +137,141 @@ public class LoanProductAdvanceExt {
@Column(name = "npa_trigger")
private String npaTrigger;
/** Months after disbursement before overdue days (SMA) start counting: Advances 9, Loans (EMI) 1. */
@Column(name = "sma_start_after_months")
private Integer smaStartAfterMonths;
/** CC/OD: stock statement older than this many days makes drawings an NPA trigger (RBI: 90). */
@Column(name = "stock_statement_validity_days")
private Integer stockStatementValidityDays;
// ---- Loan against Gold (pledged ornaments) ---------------------------
@Column(name = "gold_max_ltv_pct")
private BigDecimal goldMaxLtvPct;
@Column(name = "gold_min_purity_carat")
private Integer goldMinPurityCarat;
@Column(name = "gold_rate_source")
private String goldRateSource;
@Column(name = "gold_max_per_gram")
private BigDecimal goldMaxPerGram;
@Column(name = "gold_min_net_weight_g")
private BigDecimal goldMinNetWeightG;
@Column(name = "gold_max_net_weight_per_customer_g")
private BigDecimal goldMaxNetWeightPerCustomerG;
@Column(name = "gold_allowed_items")
private String goldAllowedItems;
@Column(name = "gold_appraiser_required")
private Boolean goldAppraiserRequired;
@Column(name = "gold_valuation_certificate_required")
private Boolean goldValuationCertificateRequired;
@Column(name = "gold_max_valuation_per_appraiser")
private BigDecimal goldMaxValuationPerAppraiser;
@Column(name = "gold_packet_prefix")
private String goldPacketPrefix;
@Column(name = "gold_auction_notice_days")
private Integer goldAuctionNoticeDays;
@Column(name = "gold_revalue_on_npa")
private Boolean goldRevalueOnNpa;
@Column(name = "gold_revaluation_charge_applicable")
private Boolean goldRevaluationChargeApplicable;
@Column(name = "gold_partial_release_allowed")
private Boolean goldPartialReleaseAllowed;
@Column(name = "gold_third_party_allowed")
private Boolean goldThirdPartyAllowed;
@Column(name = "gold_disbursement_mode")
private String goldDisbursementMode;
@Column(name = "gold_cash_disbursement_limit")
private BigDecimal goldCashDisbursementLimit;
@Column(name = "gold_ltv_breach_alert_pct")
private BigDecimal goldLtvBreachAlertPct;
@Column(name = "gold_auction_reserve_pct")
private BigDecimal goldAuctionReservePct;
@Column(name = "gold_redeem_before_auction")
private Boolean goldRedeemBeforeAuction;
// ---- Loan against Warehouse Receipt (pledged commodity) ----------------
@Column(name = "wr_max_ltv_pct")
private BigDecimal wrMaxLtvPct;
@Column(name = "wr_receipt_type")
private String wrReceiptType;
@Column(name = "wr_wdra_registered")
private Boolean wrWdraRegistered;
@Column(name = "wr_assay_certificate_required")
private Boolean wrAssayCertificateRequired;
@Column(name = "wr_tenure_within_receipt_validity")
private Boolean wrTenureWithinReceiptValidity;
@Column(name = "wr_pledge_marked_before_disbursement")
private Boolean wrPledgeMarkedBeforeDisbursement;
@Column(name = "wr_commodity_groups")
private String wrCommodityGroups;
@Column(name = "wr_valuation_price_source")
private String wrValuationPriceSource;
@Column(name = "wr_max_loan_per_quintal")
private BigDecimal wrMaxLoanPerQuintal;
@Column(name = "wr_stock_inspection_months")
private Integer wrStockInspectionMonths;
@Column(name = "wr_ltv_breach_alert_pct")
private BigDecimal wrLtvBreachAlertPct;
@Column(name = "wr_partial_release_allowed")
private Boolean wrPartialReleaseAllowed;
@Column(name = "wr_disposal_notice_days")
private Integer wrDisposalNoticeDays;
@Column(name = "wr_disposal_reserve_pct")
private BigDecimal wrDisposalReservePct;
@Column(name = "wr_redeem_before_disposal")
private Boolean wrRedeemBeforeDisposal;
// ---- common eligibility & document checklist (Advances and Loans) ----
@Column(name = "elig_member_required")
private Boolean memberRequired;
@Column(name = "elig_guarantor_must_be_member")
private Boolean guarantorMustBeMember;
@Column(name = "elig_income_multiple")
private BigDecimal incomeMultiple;
@Column(name = "elig_min_vintage_months")
private Integer minVintageMonths;
/** Comma separated document codes the borrower must submit for this product. */
@Column(name = "required_documents", length = 1000)
private String requiredDocuments;
@Column(name = "interest_after_npa")
private String interestAfterNpa;
@@ -464,6 +599,43 @@ public class LoanProductAdvanceExt {
@Column(name = "loan_balance_transfer_out_allowed")
private Boolean balanceTransferOutAllowed;
// ---- SICR & LGD (RBI 2026 ECL framework; floor % itself lives in the Prudential Floor master, not here) ----
@Column(name = "sicr_days_threshold")
private Integer sicrDaysThreshold;
@Column(name = "sicr_revolving_days_threshold")
private Integer sicrRevolvingDaysThreshold;
@Column(name = "sicr_rating_downgrade_notches")
private Integer sicrRatingDowngradeNotches;
@Column(name = "sicr_pricing_increase_trigger_pct")
private BigDecimal sicrPricingIncreaseTriggerPct;
@Column(name = "sicr_assessment_basis")
private String sicrAssessmentBasis;
@Column(name = "sicr_rebuttal_allowed")
private Boolean sicrRebuttalAllowed;
@Column(name = "npa_out_of_order_basis")
private String npaOutOfOrderBasis;
@Column(name = "crop_duration")
private String cropDuration;
@Column(name = "use_own_lgd_model")
private Boolean useOwnLgdModel;
@Column(name = "lgd_secured_pct")
private BigDecimal lgdSecuredPct;
@Column(name = "lgd_unsecured_pct")
private BigDecimal lgdUnsecuredPct;
@Column(name = "eligible_collateral_type")
private String eligibleCollateralType;
public LoanProductAdvanceExt(final LoanProduct loanProduct) {
this.loanProduct = loanProduct;
this.loanProductId = loanProduct.getId();
@@ -0,0 +1,41 @@
/**
* 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.prudentialfloor.data;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@AllArgsConstructor
@Getter
@Setter
public class PrudentialFloorData implements Serializable {
private Long id;
private String productCategory;
private Integer stage;
private Integer durationFromDays;
private Integer durationToDays;
private BigDecimal securedFloorPct;
private BigDecimal unsecuredFloorPct;
}
@@ -0,0 +1,102 @@
/**
* 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.prudentialfloor.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
/**
* One row of the RBI 2026 ECL "prudential floor" table (Directions para 82): the minimum provisioning percentage
* a bank must hold for a given loan product category and stage. Seeded centrally from the regulation rather than
* entered per product - a loan product only ever declares its category ({@code loanProductCategory} on
* {@link org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExt}) and its stage (via its
* Delinquency Bucket); the applicable floor % is always looked up here, never duplicated onto the product or the
* range.
*
* Stage 1 and Stage 2 rows apply regardless of duration ({@code durationFromDays}/{@code durationToDays} are
* null and {@code securedFloorPct} equals {@code unsecuredFloorPct}, since the Directions do not split those two
* stages by security). Stage 3 rows are further split by how long the exposure has been in Stage 3
* ({@code durationToDays} null means "and beyond") and by secured/unsecured portion.
*/
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "m_prudential_floor")
public class PrudentialFloor extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "product_category", nullable = false)
private String productCategory;
/** 1, 2 or 3. */
@Column(name = "stage", nullable = false)
private Integer stage;
@Column(name = "duration_from_days", nullable = true)
private Integer durationFromDays;
/** Null means open-ended ("after 4 years" etc.). */
@Column(name = "duration_to_days", nullable = true)
private Integer durationToDays;
@Column(name = "secured_floor_pct", nullable = false)
private BigDecimal securedFloorPct;
@Column(name = "unsecured_floor_pct", nullable = false)
private BigDecimal unsecuredFloorPct;
@Version
private Long version;
private PrudentialFloor(@NotNull String productCategory, @NotNull Integer stage, Integer durationFromDays, Integer durationToDays,
@NotNull BigDecimal securedFloorPct, @NotNull BigDecimal unsecuredFloorPct) {
this.productCategory = productCategory;
this.stage = stage;
this.durationFromDays = durationFromDays;
this.durationToDays = durationToDays;
this.securedFloorPct = securedFloorPct;
this.unsecuredFloorPct = unsecuredFloorPct;
}
public static PrudentialFloor instance(@NotNull String productCategory, @NotNull Integer stage, Integer durationFromDays,
Integer durationToDays, @NotNull BigDecimal securedFloorPct, @NotNull BigDecimal unsecuredFloorPct) {
return new PrudentialFloor(productCategory, stage, durationFromDays, durationToDays, securedFloorPct, unsecuredFloorPct);
}
/** True when {@code daysInStage} falls within this row's duration band (always true for a Stage 1/2 row). */
public boolean coversDuration(Integer daysInStage) {
if (this.durationFromDays == null) {
return true;
}
if (daysInStage == null) {
return false;
}
boolean aboveFrom = daysInStage >= this.durationFromDays;
boolean belowTo = this.durationToDays == null || daysInStage <= this.durationToDays;
return aboveFrom && belowTo;
}
}
@@ -0,0 +1,29 @@
/**
* 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.prudentialfloor.domain;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PrudentialFloorRepository extends JpaRepository<PrudentialFloor, Long> {
List<PrudentialFloor> findByProductCategoryAndStage(String productCategory, Integer stage);
List<PrudentialFloor> findByProductCategoryOrderByStageAscDurationFromDaysAsc(String productCategory);
}
@@ -0,0 +1,33 @@
/**
* 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.prudentialfloor.mapper;
import java.util.List;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.portfolio.prudentialfloor.data.PrudentialFloorData;
import org.apache.fineract.portfolio.prudentialfloor.domain.PrudentialFloor;
import org.mapstruct.Mapper;
@Mapper(config = MapstructMapperConfig.class)
public interface PrudentialFloorMapper {
PrudentialFloorData map(PrudentialFloor source);
List<PrudentialFloorData> map(List<PrudentialFloor> sources);
}
@@ -79,6 +79,17 @@ public class MakercheckersApiResource {
}
@GET
@Path("/mine")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List my pending Maker Checker Entries", description = "Requests the requestor submitted that still await approval. Unlike the checker inbox it needs no checker permission, so a maker can see what is pending. Example: makercheckers/mine?entityName=LOANPRODUCT")
public List<AuditData> retrieveMyPendingCommands(@Context final UriInfo uriInfo, @BeanParam MakerCheckerRequest makerCheckerRequest) {
final SQLBuilder extraCriteria = getExtraCriteria(makerCheckerRequest);
final ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return readPlatformService.retrieveMyPendingEntries(extraCriteria, settings.isIncludeJson());
}
@GET
@Path("/searchtemplate")
@Consumes({ MediaType.APPLICATION_JSON })
@@ -33,6 +33,9 @@ public interface AuditReadPlatformService {
List<AuditData> retrieveAllEntriesToBeChecked(SQLBuilder extraCriteria, boolean includeJson);
/** Requests the current user submitted that still await a checker, whatever the user's own checker permissions. */
List<AuditData> retrieveMyPendingEntries(SQLBuilder extraCriteria, boolean includeJson);
AuditData retrieveAuditEntry(Long auditId);
AuditSearchData retrieveSearchTemplate(String useType);
@@ -214,10 +214,17 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
return retrieveEntries("makerchecker", extraCriteria, " order by aud.id, mk.username", includeJson);
}
@Override
public List<AuditData> retrieveMyPendingEntries(final SQLBuilder extraCriteria, final boolean includeJson) {
extraCriteria.addCriteria("aud.status = ", 2);
extraCriteria.addCriteria("aud.maker_id = ", this.context.authenticatedUser().getId());
return retrieveEntries("makerchecker-mine", extraCriteria, " order by aud.id, mk.username", includeJson);
}
private List<AuditData> retrieveEntries(final String useType, final SQLBuilder extraCriteria, final String groupAndOrderBySQL,
final boolean includeJson) {
if ((!useType.equals("audit") && !useType.equals("makerchecker"))) {
if ((!useType.equals("audit") && !useType.equals("makerchecker") && !useType.equals("makerchecker-mine"))) {
throw new PlatformDataIntegrityException("error.msg.invalid.auditSearchTemplate.useType",
"Invalid Audit Search Template UseType: " + useType);
}
@@ -679,8 +679,9 @@ public final class LoanProductDataValidator {
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
// not mapped by the bank (only the nine GLs are); optional
baseDataValidator.reset().parameter(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
.value(transfersInSuspenseAccountId).ignoreIfNull().integerGreaterThanZero();
final Long incomeFromInterestId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.INTEREST_ON_LOANS.getValue(), element);
@@ -700,7 +701,7 @@ public final class LoanProductDataValidator {
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue())
.value(incomeFromRecoveryAccountId).notNull().integerGreaterThanZero();
.value(incomeFromRecoveryAccountId).ignoreIfNull().integerGreaterThanZero();
final Long writeOffAccountId = this.fromApiJsonHelper
.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
@@ -9,11 +9,15 @@ package org.apache.fineract.portfolio.loanproduct.service;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExt;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExtRepository;
@@ -43,7 +47,8 @@ public class LoanProductAdvanceExtService {
"securityType", "chargeType", "eligibleAmountBasis", "margin", "ltv", "dpMethod", "lienRequired",
"lienMarkingStage", "lienCertificateRequired", "lienCertificatePrefix", "printCertificateOnDisbursement",
"interestBasis", "spread", "interestChargedOn", "interestPostingFrequency", "penalApplicable", "penalRate",
"penalAppliedOn", "penalGraceDays", "npaTrigger", "interestAfterNpa", "undebitedInterestGlId",
"penalAppliedOn", "penalGraceDays", "npaTrigger", "smaStartAfterMonths", "stockStatementValidityDays", "memberRequired", "guarantorMustBeMember",
"incomeMultiple", "minVintageMonths", "requiredDocuments", "interestAfterNpa", "undebitedInterestGlId",
"renewalAllowed", "renewalPeriodMonths", "renewalGraceDays", "preclosureAllowed", "lockInMonths",
"interestOnPreclosure", "revaluationFreqMonths", "insuranceRequired", "stockStatementFreqMonths",
"chequeBookEnabled", "chequeType", "defaultChequeBookSize", "ppsThreshold", "ppsMandatoryAbove",
@@ -76,7 +81,17 @@ public class LoanProductAdvanceExtService {
"partPrepaymentAllowed", "minPartPayment", "allowedOn", "partPaymentsPerYear",
"partPrepaymentMode", "foreclosureFloatingIndividual", "foreclosureFixedIndividual", "foreclosureNonIndividual",
"foreclosureTakeover", "foreclosureQuoteValidDays", "autoCloseOnZeroBalance", "nocLetter",
"restructuring", "releaseCollateralOnClosure", "topUpAllowed", "balanceTransferOutAllowed" };
"restructuring", "releaseCollateralOnClosure", "topUpAllowed", "balanceTransferOutAllowed",
// SICR & LGD (RBI 2026 ECL framework)
"sicrDaysThreshold", "sicrRevolvingDaysThreshold", "sicrRatingDowngradeNotches", "sicrPricingIncreaseTriggerPct",
"sicrAssessmentBasis", "sicrRebuttalAllowed", "npaOutOfOrderBasis", "cropDuration", "useOwnLgdModel", "lgdSecuredPct",
"lgdUnsecuredPct", "eligibleCollateralType",
// Loan against Gold
"goldMaxLtv", "goldMinPurityCarat", "goldRateSource", "goldMaxPerGram", "goldMinNetWeight", "goldMaxNetWeightPerCustomer", "goldAllowedItems", "goldAppraiserRequired", "goldValuationCertificateRequired", "goldMaxValuationPerAppraiser", "goldPacketPrefix", "goldAuctionNoticeDays", "goldRevalueOnNpa", "goldRevaluationChargeApplicable",
"goldPartialReleaseAllowed", "goldThirdPartyAllowed", "goldDisbursementMode", "goldCashDisbursementLimit",
"goldLtvBreachAlert", "goldAuctionReservePct", "goldRedeemBeforeAuction",
// Loan against Warehouse Receipt
"wrMaxLtv", "wrReceiptType", "wrWdraRegistered", "wrAssayCertificateRequired", "wrTenureWithinReceiptValidity", "wrPledgeMarkedBeforeDisbursement", "wrCommodityGroups", "wrValuationPriceSource", "wrMaxLoanPerQuintal", "wrStockInspectionMonths", "wrLtvBreachAlert", "wrPartialReleaseAllowed", "wrDisposalNoticeDays", "wrDisposalReservePct", "wrRedeemBeforeDisposal" };
/**
* Insert or partial-update the satellite for this product. Returns the fields that actually changed so the
@@ -90,10 +105,205 @@ public class LoanProductAdvanceExtService {
final LoanProductAdvanceExt ext = this.repository.findByLoanProductId(loanProduct.getId())
.orElseGet(() -> new LoanProductAdvanceExt(loanProduct));
applyScalarFields(ext, command, changes);
validateGold(ext, command);
validateWarehouse(ext, command);
this.repository.save(ext);
return changes;
}
/** Longest tenure of a bullet Gold loan, in months. */
private static final int GOLD_MAX_TENURE_MONTHS = 12;
/** The most that may be paid out in cash against one gold loan (Income Tax Sec 269SS). */
private static final BigDecimal GOLD_CASH_LIMIT = BigDecimal.valueOf(20000);
/**
* Loan against Gold rules, checked on the merged state (what is stored plus what this command changes), so a
* partial edit cannot leave the product inconsistent: the LTV cap must cover the LTV the margin gives, the
* purity floor is 18 to 24 carat, and a bullet gold loan runs at most 12 months.
*/
private void validateGold(final LoanProductAdvanceExt ext, final JsonCommand command) {
if (!"gold".equals(ext.getProductTypeSubGroup())) {
return;
}
final List<ApiParameterError> errors = new ArrayList<>();
final BigDecimal hundred = BigDecimal.valueOf(100);
final BigDecimal cap = ext.getGoldMaxLtvPct();
if (cap == null || cap.signum() <= 0 || cap.compareTo(hundred) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.maxLtv.invalid",
"Maximum LTV cap is required and must be above 0 and at most 100", "goldMaxLtv"));
} else if (ext.getMarginPct() != null && hundred.subtract(ext.getMarginPct()).compareTo(cap) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.margin.ltvAboveCap",
"Margin gives an LTV above the Maximum LTV cap; raise the margin or the cap", "margin"));
}
final Integer purity = ext.getGoldMinPurityCarat();
if (purity == null || purity < 18 || purity > 24) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.minPurity.invalid",
"Minimum purity must be between 18 and 24 carat", "goldMinPurityCarat"));
}
if (isBlank(ext.getGoldRateSource())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.rateSource.required",
"Valuation rate source is required", "goldRateSource"));
}
if (isBlank(ext.getGoldAllowedItems())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.allowedItems.required",
"Allowed items is required", "goldAllowedItems"));
}
if (ext.getGoldAuctionNoticeDays() == null || ext.getGoldAuctionNoticeDays() < 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.auctionNotice.invalid",
"Auction notice period is required and cannot be negative", "goldAuctionNoticeDays"));
}
if (ext.getGoldMinNetWeightG() != null && ext.getGoldMaxNetWeightPerCustomerG() != null
&& ext.getGoldMinNetWeightG().compareTo(ext.getGoldMaxNetWeightPerCustomerG()) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.netWeight.range",
"Minimum net weight cannot exceed the maximum per customer", "goldMinNetWeight"));
}
final BigDecimal alert = ext.getGoldLtvBreachAlertPct();
if (alert == null || alert.signum() <= 0 || alert.compareTo(hundred) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.ltvAlert.invalid",
"LTV breach alert is required and must be above 0 and at most 100", "goldLtvBreachAlert"));
} else if (cap != null && alert.compareTo(cap) < 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.ltvAlert.belowCap",
"LTV breach alert cannot be below the Maximum LTV cap", "goldLtvBreachAlert"));
}
final BigDecimal reserve = ext.getGoldAuctionReservePct();
if (reserve == null || reserve.signum() <= 0 || reserve.compareTo(hundred) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.auctionReserve.invalid",
"Auction reserve price is required and must be above 0 and at most 100 percent of market value",
"goldAuctionReservePct"));
}
if (!Boolean.TRUE.equals(ext.getGoldRedeemBeforeAuction())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.redeem.mandatory",
"The customer's right to redeem before the auction cannot be switched off", "goldRedeemBeforeAuction"));
}
final String mode = ext.getGoldDisbursementMode();
if (!"account_credit".equals(mode) && !"account_credit_cash_limit".equals(mode)) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.disbursementMode.invalid",
"Disbursement mode must be account credit, or account credit with a cash limit",
"goldDisbursementMode"));
} else if ("account_credit_cash_limit".equals(mode)) {
final BigDecimal cashLimit = ext.getGoldCashDisbursementLimit();
if (cashLimit == null || cashLimit.signum() < 0 || cashLimit.compareTo(GOLD_CASH_LIMIT) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.cashLimit.invalid",
"Cash disbursement limit must be between 0 and 20,000 (Income Tax Sec 269SS)",
"goldCashDisbursementLimit"));
}
}
if (command.parameterExists("maxNumberOfRepayments")) {
final Integer maxTenure = command.integerValueOfParameterNamed("maxNumberOfRepayments");
if (maxTenure != null && maxTenure > GOLD_MAX_TENURE_MONTHS) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.gold.tenure.exceeds",
"A gold loan runs at most " + GOLD_MAX_TENURE_MONTHS + " months", "maxNumberOfRepayments"));
}
}
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException(errors);
}
}
/** Longest tenure of a warehouse receipt loan, in months. */
private static final int WAREHOUSE_MAX_TENURE_MONTHS = 12;
private static final java.util.Set<String> WR_RECEIPT_TYPES = java.util.Set.of("e_nwr", "paper_nwr", "both");
private static final java.util.Set<String> WR_COMMODITY_GROUPS = java.util.Set.of("cereals", "pulses", "oilseeds", "spices",
"others");
private static final java.util.Set<String> WR_PRICE_SOURCES = java.util.Set.of("mandi_apmc", "msp", "exchange_ncdex",
"bank_assessed");
/**
* Loan against Warehouse Receipt rules, checked on the merged state like the gold rules: the LTV cap must cover the
* LTV the margin gives, the alert level must not sit below the cap, the receipt must come from a WDRA-registered
* warehouse with an assay certificate, the customer's right to redeem stays on, and the loan runs at most 12 months.
*/
private void validateWarehouse(final LoanProductAdvanceExt ext, final JsonCommand command) {
if (!"warehouse".equals(ext.getProductTypeSubGroup())) {
return;
}
final List<ApiParameterError> errors = new ArrayList<>();
final BigDecimal hundred = BigDecimal.valueOf(100);
final BigDecimal cap = ext.getWrMaxLtvPct();
if (cap == null || cap.signum() <= 0 || cap.compareTo(hundred) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.maxLtv.invalid",
"Maximum LTV cap is required and must be above 0 and at most 100", "wrMaxLtv"));
} else if (ext.getMarginPct() != null && hundred.subtract(ext.getMarginPct()).compareTo(cap) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.margin.ltvAboveCap",
"Margin gives an LTV above the Maximum LTV cap; raise the margin or the cap", "margin"));
}
if (!WR_RECEIPT_TYPES.contains(ext.getWrReceiptType())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.receiptType.invalid",
"Receipt type must be e-NWR, paper NWR or both", "wrReceiptType"));
}
if (!Boolean.TRUE.equals(ext.getWrWdraRegistered())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.wdra.mandatory",
"The warehouse must be WDRA-registered; this cannot be switched off", "wrWdraRegistered"));
}
if (!Boolean.TRUE.equals(ext.getWrAssayCertificateRequired())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.assay.mandatory",
"A quality / assay certificate is mandatory; this cannot be switched off", "wrAssayCertificateRequired"));
}
if (isBlank(ext.getWrCommodityGroups())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.commodityGroups.required",
"Pick at least one eligible commodity group", "wrCommodityGroups"));
} else {
for (final String group : ext.getWrCommodityGroups().split("\\|")) {
if (!WR_COMMODITY_GROUPS.contains(group)) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.commodityGroups.invalid",
"Unknown commodity group: " + group, "wrCommodityGroups"));
break;
}
}
}
if (!WR_PRICE_SOURCES.contains(ext.getWrValuationPriceSource())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.priceSource.invalid",
"Valuation price source is required", "wrValuationPriceSource"));
}
if (ext.getWrMaxLoanPerQuintal() != null && ext.getWrMaxLoanPerQuintal().signum() <= 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.maxPerQuintal.invalid",
"Max loan per quintal must be above 0", "wrMaxLoanPerQuintal"));
}
if (ext.getWrStockInspectionMonths() == null || ext.getWrStockInspectionMonths() < 1) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.inspection.invalid",
"Stock inspection frequency must be at least 1 month", "wrStockInspectionMonths"));
}
final BigDecimal alert = ext.getWrLtvBreachAlertPct();
if (alert == null || alert.signum() <= 0 || alert.compareTo(hundred) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.ltvAlert.invalid",
"LTV breach alert is required and must be above 0 and at most 100", "wrLtvBreachAlert"));
} else if (cap != null && alert.compareTo(cap) < 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.ltvAlert.belowCap",
"LTV breach alert cannot be below the Maximum LTV cap", "wrLtvBreachAlert"));
}
if (ext.getWrDisposalNoticeDays() == null || ext.getWrDisposalNoticeDays() < 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.disposalNotice.invalid",
"Disposal notice period is required and cannot be negative", "wrDisposalNoticeDays"));
}
final BigDecimal reserve = ext.getWrDisposalReservePct();
if (reserve == null || reserve.signum() <= 0 || reserve.compareTo(hundred) > 0) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.disposalReserve.invalid",
"Disposal reserve price is required and must be above 0 and at most 100 percent of market value",
"wrDisposalReservePct"));
}
if (!Boolean.TRUE.equals(ext.getWrRedeemBeforeDisposal())) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.redeem.mandatory",
"The customer's right to redeem before the disposal cannot be switched off", "wrRedeemBeforeDisposal"));
}
if (command.parameterExists("maxNumberOfRepayments")) {
final Integer maxTenure = command.integerValueOfParameterNamed("maxNumberOfRepayments");
if (maxTenure != null && maxTenure > WAREHOUSE_MAX_TENURE_MONTHS) {
errors.add(ApiParameterError.parameterError("validation.msg.loanproduct.warehouse.tenure.exceeds",
"A warehouse receipt loan runs at most " + WAREHOUSE_MAX_TENURE_MONTHS + " months",
"maxNumberOfRepayments"));
}
}
if (!errors.isEmpty()) {
throw new PlatformApiDataValidationException(errors);
}
}
private static boolean isBlank(final String value) {
return value == null || value.isBlank();
}
public Map<String, Object> retrieveAsMap(final Long loanProductId) {
return this.repository.findByLoanProductId(loanProductId).map(LoanProductAdvanceExtService::toMap)
.orElseGet(LinkedHashMap::new);
@@ -155,6 +365,14 @@ public class LoanProductAdvanceExtService {
setString(command, "penalAppliedOn", ext::getPenalAppliedOn, ext::setPenalAppliedOn, changes);
setInteger(command, "penalGraceDays", ext::getPenalGraceDays, ext::setPenalGraceDays, changes);
setString(command, "npaTrigger", ext::getNpaTrigger, ext::setNpaTrigger, changes);
setInteger(command, "smaStartAfterMonths", ext::getSmaStartAfterMonths, ext::setSmaStartAfterMonths, changes);
setBoolean(command, "memberRequired", ext::getMemberRequired, ext::setMemberRequired, changes);
setBoolean(command, "guarantorMustBeMember", ext::getGuarantorMustBeMember, ext::setGuarantorMustBeMember, changes);
setDecimal(command, "incomeMultiple", ext::getIncomeMultiple, ext::setIncomeMultiple, changes);
setInteger(command, "minVintageMonths", ext::getMinVintageMonths, ext::setMinVintageMonths, changes);
setString(command, "requiredDocuments", ext::getRequiredDocuments, ext::setRequiredDocuments, changes);
setInteger(command, "stockStatementValidityDays", ext::getStockStatementValidityDays,
ext::setStockStatementValidityDays, 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);
@@ -163,6 +381,48 @@ public class LoanProductAdvanceExtService {
setString(command, "preclosureAllowed", ext::getPreclosureAllowed, ext::setPreclosureAllowed, changes);
setInteger(command, "lockInMonths", ext::getLockInMonths, ext::setLockInMonths, changes);
setString(command, "interestOnPreclosure", ext::getInterestOnPreclosure, ext::setInterestOnPreclosure, changes);
setDecimal(command, "goldMaxLtv", ext::getGoldMaxLtvPct, ext::setGoldMaxLtvPct, changes);
setInteger(command, "goldMinPurityCarat", ext::getGoldMinPurityCarat, ext::setGoldMinPurityCarat, changes);
setString(command, "goldRateSource", ext::getGoldRateSource, ext::setGoldRateSource, changes);
setDecimal(command, "goldMaxPerGram", ext::getGoldMaxPerGram, ext::setGoldMaxPerGram, changes);
setDecimal(command, "goldMinNetWeight", ext::getGoldMinNetWeightG, ext::setGoldMinNetWeightG, changes);
setDecimal(command, "goldMaxNetWeightPerCustomer", ext::getGoldMaxNetWeightPerCustomerG, ext::setGoldMaxNetWeightPerCustomerG, changes);
setString(command, "goldAllowedItems", ext::getGoldAllowedItems, ext::setGoldAllowedItems, changes);
setBoolean(command, "goldAppraiserRequired", ext::getGoldAppraiserRequired, ext::setGoldAppraiserRequired, changes);
setBoolean(command, "goldValuationCertificateRequired", ext::getGoldValuationCertificateRequired, ext::setGoldValuationCertificateRequired, changes);
setDecimal(command, "goldMaxValuationPerAppraiser", ext::getGoldMaxValuationPerAppraiser, ext::setGoldMaxValuationPerAppraiser, changes);
setString(command, "goldPacketPrefix", ext::getGoldPacketPrefix, ext::setGoldPacketPrefix, changes);
setInteger(command, "goldAuctionNoticeDays", ext::getGoldAuctionNoticeDays, ext::setGoldAuctionNoticeDays, changes);
setBoolean(command, "goldRevalueOnNpa", ext::getGoldRevalueOnNpa, ext::setGoldRevalueOnNpa, changes);
setBoolean(command, "goldRevaluationChargeApplicable", ext::getGoldRevaluationChargeApplicable,
ext::setGoldRevaluationChargeApplicable, changes);
setBoolean(command, "goldPartialReleaseAllowed", ext::getGoldPartialReleaseAllowed,
ext::setGoldPartialReleaseAllowed, changes);
setBoolean(command, "goldThirdPartyAllowed", ext::getGoldThirdPartyAllowed, ext::setGoldThirdPartyAllowed,
changes);
setString(command, "goldDisbursementMode", ext::getGoldDisbursementMode, ext::setGoldDisbursementMode, changes);
setDecimal(command, "goldCashDisbursementLimit", ext::getGoldCashDisbursementLimit,
ext::setGoldCashDisbursementLimit, changes);
setDecimal(command, "goldLtvBreachAlert", ext::getGoldLtvBreachAlertPct, ext::setGoldLtvBreachAlertPct, changes);
setDecimal(command, "goldAuctionReservePct", ext::getGoldAuctionReservePct, ext::setGoldAuctionReservePct,
changes);
setBoolean(command, "goldRedeemBeforeAuction", ext::getGoldRedeemBeforeAuction, ext::setGoldRedeemBeforeAuction,
changes);
setDecimal(command, "wrMaxLtv", ext::getWrMaxLtvPct, ext::setWrMaxLtvPct, changes);
setString(command, "wrReceiptType", ext::getWrReceiptType, ext::setWrReceiptType, changes);
setBoolean(command, "wrWdraRegistered", ext::getWrWdraRegistered, ext::setWrWdraRegistered, changes);
setBoolean(command, "wrAssayCertificateRequired", ext::getWrAssayCertificateRequired, ext::setWrAssayCertificateRequired, changes);
setBoolean(command, "wrTenureWithinReceiptValidity", ext::getWrTenureWithinReceiptValidity, ext::setWrTenureWithinReceiptValidity, changes);
setBoolean(command, "wrPledgeMarkedBeforeDisbursement", ext::getWrPledgeMarkedBeforeDisbursement, ext::setWrPledgeMarkedBeforeDisbursement, changes);
setString(command, "wrCommodityGroups", ext::getWrCommodityGroups, ext::setWrCommodityGroups, changes);
setString(command, "wrValuationPriceSource", ext::getWrValuationPriceSource, ext::setWrValuationPriceSource, changes);
setDecimal(command, "wrMaxLoanPerQuintal", ext::getWrMaxLoanPerQuintal, ext::setWrMaxLoanPerQuintal, changes);
setInteger(command, "wrStockInspectionMonths", ext::getWrStockInspectionMonths, ext::setWrStockInspectionMonths, changes);
setDecimal(command, "wrLtvBreachAlert", ext::getWrLtvBreachAlertPct, ext::setWrLtvBreachAlertPct, changes);
setBoolean(command, "wrPartialReleaseAllowed", ext::getWrPartialReleaseAllowed, ext::setWrPartialReleaseAllowed, changes);
setInteger(command, "wrDisposalNoticeDays", ext::getWrDisposalNoticeDays, ext::setWrDisposalNoticeDays, changes);
setDecimal(command, "wrDisposalReservePct", ext::getWrDisposalReservePct, ext::setWrDisposalReservePct, changes);
setBoolean(command, "wrRedeemBeforeDisposal", ext::getWrRedeemBeforeDisposal, ext::setWrRedeemBeforeDisposal, changes);
setInteger(command, "revaluationFreqMonths", ext::getRevaluationFreqMonths, ext::setRevaluationFreqMonths,
changes);
setBoolean(command, "insuranceRequired", ext::getInsuranceRequired, ext::setInsuranceRequired, changes);
@@ -289,6 +549,20 @@ public class LoanProductAdvanceExtService {
setBoolean(command, "releaseCollateralOnClosure", ext::getReleaseCollateralOnClosure, ext::setReleaseCollateralOnClosure, changes);
setBoolean(command, "topUpAllowed", ext::getTopUpAllowed, ext::setTopUpAllowed, changes);
setBoolean(command, "balanceTransferOutAllowed", ext::getBalanceTransferOutAllowed, ext::setBalanceTransferOutAllowed, changes);
// SICR & LGD (RBI 2026 ECL framework)
setInteger(command, "sicrDaysThreshold", ext::getSicrDaysThreshold, ext::setSicrDaysThreshold, changes);
setInteger(command, "sicrRevolvingDaysThreshold", ext::getSicrRevolvingDaysThreshold, ext::setSicrRevolvingDaysThreshold, changes);
setInteger(command, "sicrRatingDowngradeNotches", ext::getSicrRatingDowngradeNotches, ext::setSicrRatingDowngradeNotches, changes);
setDecimal(command, "sicrPricingIncreaseTriggerPct", ext::getSicrPricingIncreaseTriggerPct, ext::setSicrPricingIncreaseTriggerPct,
changes);
setString(command, "sicrAssessmentBasis", ext::getSicrAssessmentBasis, ext::setSicrAssessmentBasis, changes);
setBoolean(command, "sicrRebuttalAllowed", ext::getSicrRebuttalAllowed, ext::setSicrRebuttalAllowed, changes);
setString(command, "npaOutOfOrderBasis", ext::getNpaOutOfOrderBasis, ext::setNpaOutOfOrderBasis, changes);
setString(command, "cropDuration", ext::getCropDuration, ext::setCropDuration, changes);
setBoolean(command, "useOwnLgdModel", ext::getUseOwnLgdModel, ext::setUseOwnLgdModel, changes);
setDecimal(command, "lgdSecuredPct", ext::getLgdSecuredPct, ext::setLgdSecuredPct, changes);
setDecimal(command, "lgdUnsecuredPct", ext::getLgdUnsecuredPct, ext::setLgdUnsecuredPct, changes);
setString(command, "eligibleCollateralType", ext::getEligibleCollateralType, ext::setEligibleCollateralType, changes);
}
// ---- typed setters: only write params that are present, and only report real changes ----------
@@ -403,6 +677,13 @@ public class LoanProductAdvanceExtService {
map.put("penalAppliedOn", ext.getPenalAppliedOn());
map.put("penalGraceDays", ext.getPenalGraceDays());
map.put("npaTrigger", ext.getNpaTrigger());
map.put("smaStartAfterMonths", ext.getSmaStartAfterMonths());
map.put("memberRequired", ext.getMemberRequired());
map.put("guarantorMustBeMember", ext.getGuarantorMustBeMember());
map.put("incomeMultiple", ext.getIncomeMultiple());
map.put("minVintageMonths", ext.getMinVintageMonths());
map.put("requiredDocuments", ext.getRequiredDocuments());
map.put("stockStatementValidityDays", ext.getStockStatementValidityDays());
map.put("interestAfterNpa", ext.getInterestAfterNpa());
map.put("undebitedInterestGlId", ext.getUndebitedInterestGlId());
map.put("renewalAllowed", ext.getRenewalAllowed());
@@ -411,6 +692,42 @@ public class LoanProductAdvanceExtService {
map.put("preclosureAllowed", ext.getPreclosureAllowed());
map.put("lockInMonths", ext.getLockInMonths());
map.put("interestOnPreclosure", ext.getInterestOnPreclosure());
map.put("goldMaxLtv", ext.getGoldMaxLtvPct());
map.put("goldMinPurityCarat", ext.getGoldMinPurityCarat());
map.put("goldRateSource", ext.getGoldRateSource());
map.put("goldMaxPerGram", ext.getGoldMaxPerGram());
map.put("goldMinNetWeight", ext.getGoldMinNetWeightG());
map.put("goldMaxNetWeightPerCustomer", ext.getGoldMaxNetWeightPerCustomerG());
map.put("goldAllowedItems", ext.getGoldAllowedItems());
map.put("goldAppraiserRequired", ext.getGoldAppraiserRequired());
map.put("goldValuationCertificateRequired", ext.getGoldValuationCertificateRequired());
map.put("goldMaxValuationPerAppraiser", ext.getGoldMaxValuationPerAppraiser());
map.put("goldPacketPrefix", ext.getGoldPacketPrefix());
map.put("goldAuctionNoticeDays", ext.getGoldAuctionNoticeDays());
map.put("goldRevalueOnNpa", ext.getGoldRevalueOnNpa());
map.put("goldRevaluationChargeApplicable", ext.getGoldRevaluationChargeApplicable());
map.put("goldPartialReleaseAllowed", ext.getGoldPartialReleaseAllowed());
map.put("goldThirdPartyAllowed", ext.getGoldThirdPartyAllowed());
map.put("goldDisbursementMode", ext.getGoldDisbursementMode());
map.put("goldCashDisbursementLimit", ext.getGoldCashDisbursementLimit());
map.put("goldLtvBreachAlert", ext.getGoldLtvBreachAlertPct());
map.put("goldAuctionReservePct", ext.getGoldAuctionReservePct());
map.put("goldRedeemBeforeAuction", ext.getGoldRedeemBeforeAuction());
map.put("wrMaxLtv", ext.getWrMaxLtvPct());
map.put("wrReceiptType", ext.getWrReceiptType());
map.put("wrWdraRegistered", ext.getWrWdraRegistered());
map.put("wrAssayCertificateRequired", ext.getWrAssayCertificateRequired());
map.put("wrTenureWithinReceiptValidity", ext.getWrTenureWithinReceiptValidity());
map.put("wrPledgeMarkedBeforeDisbursement", ext.getWrPledgeMarkedBeforeDisbursement());
map.put("wrCommodityGroups", ext.getWrCommodityGroups());
map.put("wrValuationPriceSource", ext.getWrValuationPriceSource());
map.put("wrMaxLoanPerQuintal", ext.getWrMaxLoanPerQuintal());
map.put("wrStockInspectionMonths", ext.getWrStockInspectionMonths());
map.put("wrLtvBreachAlert", ext.getWrLtvBreachAlertPct());
map.put("wrPartialReleaseAllowed", ext.getWrPartialReleaseAllowed());
map.put("wrDisposalNoticeDays", ext.getWrDisposalNoticeDays());
map.put("wrDisposalReservePct", ext.getWrDisposalReservePct());
map.put("wrRedeemBeforeDisposal", ext.getWrRedeemBeforeDisposal());
map.put("revaluationFreqMonths", ext.getRevaluationFreqMonths());
map.put("insuranceRequired", ext.getInsuranceRequired());
map.put("stockStatementFreqMonths", ext.getStockStatementFreqMonths());
@@ -509,6 +826,19 @@ public class LoanProductAdvanceExtService {
map.put("releaseCollateralOnClosure", ext.getReleaseCollateralOnClosure());
map.put("topUpAllowed", ext.getTopUpAllowed());
map.put("balanceTransferOutAllowed", ext.getBalanceTransferOutAllowed());
// SICR & LGD (RBI 2026 ECL framework)
map.put("sicrDaysThreshold", ext.getSicrDaysThreshold());
map.put("sicrRevolvingDaysThreshold", ext.getSicrRevolvingDaysThreshold());
map.put("sicrRatingDowngradeNotches", ext.getSicrRatingDowngradeNotches());
map.put("sicrPricingIncreaseTriggerPct", ext.getSicrPricingIncreaseTriggerPct());
map.put("sicrAssessmentBasis", ext.getSicrAssessmentBasis());
map.put("sicrRebuttalAllowed", ext.getSicrRebuttalAllowed());
map.put("npaOutOfOrderBasis", ext.getNpaOutOfOrderBasis());
map.put("cropDuration", ext.getCropDuration());
map.put("useOwnLgdModel", ext.getUseOwnLgdModel());
map.put("lgdSecuredPct", ext.getLgdSecuredPct());
map.put("lgdUnsecuredPct", ext.getLgdUnsecuredPct());
map.put("eligibleCollateralType", ext.getEligibleCollateralType());
return map;
}
}
@@ -0,0 +1,63 @@
/**
* 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.prudentialfloor.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.prudentialfloor.data.PrudentialFloorData;
import org.apache.fineract.portfolio.prudentialfloor.service.PrudentialFloorReadPlatformService;
import org.springframework.stereotype.Component;
/**
* Read-only: this master is seeded from the RBI 2026 Directions (para 82) rather than created per bank, so it
* ships without a create/update command path for now - a Board-approved "tighten only" override is a follow-up.
*/
@RequiredArgsConstructor
@Path("/v1/prudentialfloors")
@Component
@Tag(name = "Prudential Floor Master", description = "RBI 2026 ECL stage-wise prudential floor lookup, by loan product category")
public class PrudentialFloorsApiResource {
private final PlatformSecurityContext securityContext;
private final PrudentialFloorReadPlatformService readPlatformService;
public static final String PRUDENTIAL_FLOOR = "PRUDENTIAL_FLOOR";
@GET
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "List all Prudential Floor rows", description = "Optionally filter by product category")
public List<PrudentialFloorData> getPrudentialFloors(
@QueryParam("productCategory") @Parameter(description = "productCategory") final String productCategory) {
securityContext.authenticatedUser().validateHasReadPermission(PRUDENTIAL_FLOOR);
if (productCategory == null || productCategory.isBlank()) {
return this.readPlatformService.retrieveAll();
}
return this.readPlatformService.retrieveForCategory(productCategory);
}
}
@@ -0,0 +1,63 @@
/**
* 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.prudentialfloor.service;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.prudentialfloor.data.PrudentialFloorData;
import org.apache.fineract.portfolio.prudentialfloor.domain.PrudentialFloor;
import org.apache.fineract.portfolio.prudentialfloor.domain.PrudentialFloorRepository;
import org.apache.fineract.portfolio.prudentialfloor.mapper.PrudentialFloorMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Read-only lookup over the RBI 2026 prudential-floor master (para 82). A loan product never stores its own
* floor %: it declares a product category and a stage (the latter via its Delinquency Bucket), and the
* applicable floor is resolved here at reporting/provisioning time, so every product using the same category
* always sees the same, currently-seeded regulatory minimum.
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PrudentialFloorReadPlatformService {
private final PrudentialFloorRepository repository;
private final PrudentialFloorMapper mapper;
public List<PrudentialFloorData> retrieveAll() {
return mapper.map(repository.findAll());
}
public List<PrudentialFloorData> retrieveForCategory(final String productCategory) {
return mapper.map(repository.findByProductCategoryOrderByStageAscDurationFromDaysAsc(productCategory));
}
/**
* The floor applicable to one exposure: for Stage 1/2 the single row for that category+stage; for Stage 3
* the row whose duration band contains {@code daysInStage} (days since the exposure entered Stage 3, not
* days overdue). Returns {@code null} when the category/stage/duration combination has not been seeded -
* callers should fall back to the residual "any other loan product category" floor (para 82 item (xv)) in
* that case rather than treat it as zero.
*/
public PrudentialFloorData retrieveFloor(final String productCategory, final Integer stage, final Integer daysInStage) {
final List<PrudentialFloor> candidates = repository.findByProductCategoryAndStage(productCategory, stage);
return candidates.stream().filter(row -> row.coversDuration(daysInStage)).findFirst().map(mapper::map).orElse(null);
}
}
@@ -623,6 +623,17 @@
<include file="parts/0595_seed_loan_rate_register_gl_accounts.xml" relativeToChangelogFile="true"/>
<include file="parts/0596_enable_delinquency_maker_checker.xml" relativeToChangelogFile="true"/>
<include file="parts/0597_add_loan_product_loans_ext_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0598_add_loan_product_sma_start_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0599_enable_loan_product_update_maker_checker.xml" relativeToChangelogFile="true"/>
<!-- RBI 2026 ECL framework: SMA/NPA asset classification, prudential floor master, SICR/LGD product config. -->
<include file="parts/0599_add_delinquency_range_stage.xml" relativeToChangelogFile="true"/>
<include file="parts/0600_create_prudential_floor_master.xml" relativeToChangelogFile="true"/>
<include file="parts/0601_add_loan_product_advance_ext_sicr_lgd_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0604_add_gold_loan_product_fields.xml" relativeToChangelogFile="true"/>
<include file="parts/0605_add_gold_revaluation_rules.xml" relativeToChangelogFile="true"/>
<include file="parts/0606_add_gold_servicing_and_auction_rules.xml" relativeToChangelogFile="true"/>
<include file="parts/0607_add_warehouse_receipt_product_fields.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. -->
@@ -0,0 +1,39 @@
<?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">
<!-- SMA counting start (Advances: 9 months after disbursement, Loans/EMI: 1 month) and the CC/OD stock
statement validity window that feeds the NPA trigger. -->
<changeSet author="cbsnexora" id="0598_add_loan_product_sma_start_fields">
<addColumn tableName="m_product_loan_advance_ext">
<column name="sma_start_after_months" type="INT"/>
<column name="stock_statement_validity_days" type="INT"/>
<column name="elig_member_required" type="BOOLEAN"/>
<column name="elig_guarantor_must_be_member" type="BOOLEAN"/>
<column name="elig_income_multiple" type="DECIMAL(10,2)"/>
<column name="elig_min_vintage_months" type="INT"/>
<column name="required_documents" type="VARCHAR(1000)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,35 @@
<?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">
<!-- RBI 2026 ECL framework: a delinquency range only ever decided an SMA/NPA label by day-count; it now also
records which ECL stage (1, 2 or 3) that label maps to, so a product's bucket can drive staging directly
instead of a parallel lookup. Nullable: existing ranges keep working as plain labels until re-classified. -->
<changeSet author="cbsnexora" id="0599_add_delinquency_range_stage">
<addColumn tableName="m_delinquency_range">
<column name="stage" type="INT" defaultValueComputed="NULL"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,45 @@
<?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">
<!--
Maker-checker on editing a loan or advance product.
Creating a product already goes to a checker (CREATE_LOANPRODUCT is gated). An edit changes rates,
limits, margin, GL accounts and lien rules that live accounts depend on, so it must not take effect on
one person's click either. UPDATE_LOANPRODUCT is gated with can_maker_checker = true: an edit lands in
m_portfolio_command_source as PENDING and is applied when a checker approves it. UPDATE_LOANPRODUCT_CHECKER
already exists and is what approving requires. Roles are left as they are.
-->
<changeSet id="0599_loan_product_update_maker_checker" author="cbsnexora" runOnChange="false">
<validCheckSum>ANY</validCheckSum>
<sql>
UPDATE m_permission
SET can_maker_checker = TRUE
WHERE code = 'UPDATE_LOANPRODUCT';
</sql>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,167 @@
<?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">
<!-- RBI 2026 ECL framework (Directions para 82): the stage-wise prudential floor, keyed by loan product
category rather than by loan product. Seeded below with a representative set of categories; the
remaining categories and the category-specific Stage 3 table variants (unsecured retail, housing/other
real estate, government-guaranteed) are a follow-up seed, not a schema change. created_by/
last_modified_by use 1 (the system user) since this is regulatory data, not something a maker entered. -->
<changeSet author="cbsnexora" id="0600_create_prudential_floor_master_table">
<createTable tableName="m_prudential_floor">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="product_category" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="stage" type="INT">
<constraints nullable="false"/>
</column>
<column name="duration_from_days" type="INT" defaultValueComputed="NULL"/>
<column name="duration_to_days" type="INT" defaultValueComputed="NULL"/>
<column name="secured_floor_pct" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="unsecured_floor_pct" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="created_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
<column name="version" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex indexName="IX_m_prudential_floor_category_stage" tableName="m_prudential_floor">
<column name="product_category"/>
<column name="stage"/>
</createIndex>
</changeSet>
<changeSet author="cbsnexora" id="0600_add_prudential_floor_permission">
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="READ_PRUDENTIAL_FLOOR"/>
<column name="entity_name" value="PRUDENTIAL_FLOOR"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="cbsnexora" id="0600_seed_prudential_floor_stage_1_2">
<!-- Stage 1 / Stage 2: a single floor per category (not split secured/unsecured in the Directions),
so secured_floor_pct = unsecured_floor_pct here. -->
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.40"/><column name="unsecured_floor_pct" valueNumeric="0.40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="5.00"/><column name="unsecured_floor_pct" valueNumeric="5.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.40"/><column name="unsecured_floor_pct" valueNumeric="0.40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="5.00"/><column name="unsecured_floor_pct" valueNumeric="5.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.25"/><column name="unsecured_floor_pct" valueNumeric="0.25"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="5.00"/><column name="unsecured_floor_pct" valueNumeric="5.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.40"/><column name="unsecured_floor_pct" valueNumeric="0.40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="5.00"/><column name="unsecured_floor_pct" valueNumeric="5.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.25"/><column name="unsecured_floor_pct" valueNumeric="0.25"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="5.00"/><column name="unsecured_floor_pct" valueNumeric="5.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Unsecured Retail Loans"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="1.00"/><column name="unsecured_floor_pct" valueNumeric="1.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Unsecured Retail Loans"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="5.00"/><column name="unsecured_floor_pct" valueNumeric="5.00"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.40"/><column name="unsecured_floor_pct" valueNumeric="0.40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="1.50"/><column name="unsecured_floor_pct" valueNumeric="1.50"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.25"/><column name="unsecured_floor_pct" valueNumeric="0.25"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="1.50"/><column name="unsecured_floor_pct" valueNumeric="1.50"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Loan against Term Deposit / LIC / KVP"/><column name="stage" valueNumeric="1"/><column name="secured_floor_pct" valueNumeric="0.40"/><column name="unsecured_floor_pct" valueNumeric="0.40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Loan against Term Deposit / LIC / KVP"/><column name="stage" valueNumeric="2"/><column name="secured_floor_pct" valueNumeric="0.40"/><column name="unsecured_floor_pct" valueNumeric="0.40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
</changeSet>
<changeSet author="cbsnexora" id="0600_seed_prudential_floor_stage_3_standard">
<!-- Stage 3 ("standard" duration table, para 82(5) first table): applied here to every seeded category
alike as a starting point. The Directions in fact vary this table by category group (housing/real
estate and unsecured retail use their own bands) - narrowing this per category is a follow-up seed,
not a schema change, since duration_from_days/duration_to_days already carry whatever band a row
needs. -->
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="55"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="75"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Secured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="55"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="75"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Corporate Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="55"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="75"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Small and Micro Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="55"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="75"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Medium Enterprises"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="55"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="75"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Farm Credit - Agricultural Activities"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="40"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="55"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="75"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Gold Loan"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="10"/><column name="unsecured_floor_pct" valueNumeric="25"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="duration_to_days" valueNumeric="730"/><column name="secured_floor_pct" valueNumeric="20"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="731"/><column name="duration_to_days" valueNumeric="1095"/><column name="secured_floor_pct" valueNumeric="30"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1096"/><column name="duration_to_days" valueNumeric="1460"/><column name="secured_floor_pct" valueNumeric="40"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Housing Loan - Individual"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="1461"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Unsecured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="duration_to_days" valueNumeric="365"/><column name="secured_floor_pct" valueNumeric="25"/><column name="unsecured_floor_pct" valueNumeric="25"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Unsecured Retail Loans"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="366"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
<insert tableName="m_prudential_floor"><column name="product_category" value="Loan against Term Deposit / LIC / KVP"/><column name="stage" valueNumeric="3"/><column name="duration_from_days" valueNumeric="0"/><column name="secured_floor_pct" valueNumeric="100"/><column name="unsecured_floor_pct" valueNumeric="100"/><column name="created_by" valueNumeric="1"/><column name="created_on_utc" valueComputed="NOW()"/><column name="version" valueNumeric="1"/><column name="last_modified_by" valueNumeric="1"/><column name="last_modified_on_utc" valueComputed="NOW()"/></insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,49 @@
<?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">
<!-- RBI 2026 ECL framework, Penal/SMA/NPA wizard step: SICR trigger configuration and the LGD backstop /
own-model override. The prudential floor % itself is NOT stored here - it is looked up from
m_prudential_floor by product category, so every product stays in sync with a single regulatory table. -->
<changeSet author="cbsnexora" id="0601_add_loan_product_advance_ext_sicr_lgd_fields">
<addColumn tableName="m_product_loan_advance_ext">
<!-- SICR trigger configuration -->
<column name="sicr_days_threshold" type="INT"/>
<column name="sicr_revolving_days_threshold" type="INT"/>
<column name="sicr_rating_downgrade_notches" type="INT"/>
<column name="sicr_pricing_increase_trigger_pct" type="DECIMAL(19,6)"/>
<column name="sicr_assessment_basis" type="VARCHAR(30)"/>
<column name="sicr_rebuttal_allowed" type="BOOLEAN"/>
<!-- NPA trigger extras -->
<column name="npa_out_of_order_basis" type="VARCHAR(60)"/>
<column name="crop_duration" type="VARCHAR(20)"/>
<!-- LGD backstop / override -->
<column name="use_own_lgd_model" type="BOOLEAN"/>
<column name="lgd_secured_pct" type="DECIMAL(19,6)"/>
<column name="lgd_unsecured_pct" type="DECIMAL(19,6)"/>
<column name="eligible_collateral_type" type="VARCHAR(200)"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,43 @@
<?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">
<!-- Loan against Gold: valuation basis, appraiser rule, safe custody and auction notice. Pledge (not lien) security. -->
<changeSet author="cbsnexora" id="0604_add_gold_loan_product_fields">
<addColumn tableName="m_product_loan_advance_ext">
<column name="gold_max_ltv_pct" type="DECIMAL(6,2)"/>
<column name="gold_min_purity_carat" type="INT"/>
<column name="gold_rate_source" type="VARCHAR(40)"/>
<column name="gold_max_per_gram" type="DECIMAL(19,2)"/>
<column name="gold_min_net_weight_g" type="DECIMAL(10,2)"/>
<column name="gold_max_net_weight_per_customer_g" type="DECIMAL(10,2)"/>
<column name="gold_allowed_items" type="VARCHAR(40)"/>
<column name="gold_appraiser_required" type="BOOLEAN"/>
<column name="gold_valuation_certificate_required" type="BOOLEAN"/>
<column name="gold_max_valuation_per_appraiser" type="DECIMAL(19,2)"/>
<column name="gold_packet_prefix" type="VARCHAR(20)"/>
<column name="gold_auction_notice_days" type="INT"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,34 @@
<?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">
<!-- Gold loans are short term, so the gold is not revalued on a schedule: it is revalued when the loan turns NPA,
and the customer may be charged for it. -->
<changeSet author="cbsnexora" id="0605_add_gold_revaluation_rules">
<addColumn tableName="m_product_loan_advance_ext">
<column name="gold_revalue_on_npa" type="BOOLEAN"/>
<column name="gold_revaluation_charge_applicable" type="BOOLEAN"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,39 @@
<?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">
<!-- Loan against Gold: servicing rules (partial release, third-party gold), disbursement mode with the cash limit,
the LTV level that flags a falling gold rate, and the auction reserve price / right to redeem. -->
<changeSet author="cbsnexora" id="0606_add_gold_servicing_and_auction_rules">
<addColumn tableName="m_product_loan_advance_ext">
<column name="gold_partial_release_allowed" type="BOOLEAN"/>
<column name="gold_third_party_allowed" type="BOOLEAN"/>
<column name="gold_disbursement_mode" type="VARCHAR(40)"/>
<column name="gold_cash_disbursement_limit" type="DECIMAL(19,2)"/>
<column name="gold_ltv_breach_alert_pct" type="DECIMAL(6,2)"/>
<column name="gold_auction_reserve_pct" type="DECIMAL(6,2)"/>
<column name="gold_redeem_before_auction" type="BOOLEAN"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,47 @@
<?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">
<!-- Loan against Warehouse Receipt: the receipt rules (type, WDRA warehouse, assay, validity, repository pledge),
the commodity groups and valuation price, stock inspection, LTV alert, and the disposal rules after NPA. -->
<changeSet author="cbsnexora" id="0607_add_warehouse_receipt_product_fields">
<addColumn tableName="m_product_loan_advance_ext">
<column name="wr_max_ltv_pct" type="DECIMAL(6,2)"/>
<column name="wr_receipt_type" type="VARCHAR(20)"/>
<column name="wr_wdra_registered" type="BOOLEAN"/>
<column name="wr_assay_certificate_required" type="BOOLEAN"/>
<column name="wr_tenure_within_receipt_validity" type="BOOLEAN"/>
<column name="wr_pledge_marked_before_disbursement" type="BOOLEAN"/>
<column name="wr_commodity_groups" type="VARCHAR(200)"/>
<column name="wr_valuation_price_source" type="VARCHAR(30)"/>
<column name="wr_max_loan_per_quintal" type="DECIMAL(19,2)"/>
<column name="wr_stock_inspection_months" type="INT"/>
<column name="wr_ltv_breach_alert_pct" type="DECIMAL(6,2)"/>
<column name="wr_partial_release_allowed" type="BOOLEAN"/>
<column name="wr_disposal_notice_days" type="INT"/>
<column name="wr_disposal_reserve_pct" type="DECIMAL(6,2)"/>
<column name="wr_redeem_before_disposal" type="BOOLEAN"/>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -173,7 +173,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
when(loanAccountData.getId()).thenReturn(1L);
when(loanAccountData.getAccountNo()).thenReturn("0001");
when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10));
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10, null));
when(loanAccountData.getCurrency()).thenAnswer(
a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf()));
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
@@ -238,7 +238,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
when(loanAccountData.getId()).thenReturn(1L);
when(loanAccountData.getAccountNo()).thenReturn("0001");
when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10));
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10, null));
when(loanAccountData.getCurrency()).thenAnswer(
a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf()));
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
@@ -174,9 +174,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
@@ -217,9 +217,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
@@ -281,9 +281,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
@@ -346,9 +346,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
@@ -425,9 +425,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
@@ -510,9 +510,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
// given
Loan loanForProcessing = Mockito.mock(Loan.class);
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
@@ -597,9 +597,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
// given
Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
range1.setId(1L);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
range2.setId(2L);
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");