implemented the Edit prodict functionality for the RD,FD,DDS,LIC

This commit is contained in:
2026-10-16 10:33:57 +05:30
parent 0afcf059a8
commit 4c40784a0e
28 changed files with 874 additions and 32 deletions
@@ -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,31 @@ 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;
// ---- 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 +489,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);
}