From 4c40784a0e15bd43eb6ea8ed50b539f60884598c Mon Sep 17 00:00:00 2001 From: Sonali Likhitkar Date: Fri, 16 Oct 2026 10:33:57 +0530 Subject: [PATCH] implemented the Edit prodict functionality for the RD,FD,DDS,LIC --- ...AccountMappingFromApiJsonDeserializer.java | 4 +- .../delinquency/domain/DelinquencyRange.java | 16 +- .../LoanProductToGLAccountMappingHelper.java | 17 ++ .../api/DelinquencyApiConstants.java | 1 + .../api/DelinquencyRangeRequest.java | 1 + .../data/DelinquencyRangeData.java | 8 +- .../DelinquencyWritePlatformServiceImpl.java | 6 +- .../DelinquencyRangeParseAndValidator.java | 7 +- .../domain/LoanProductAdvanceExt.java | 62 +++++++ .../data/PrudentialFloorData.java | 41 +++++ .../domain/PrudentialFloor.java | 102 +++++++++++ .../domain/PrudentialFloorRepository.java | 29 +++ .../mapper/PrudentialFloorMapper.java | 33 ++++ .../api/MakercheckersApiResource.java | 11 ++ .../service/AuditReadPlatformService.java | 3 + .../service/AuditReadPlatformServiceImpl.java | 9 +- .../LoanProductDataValidator.java | 5 +- .../service/LoanProductAdvanceExtService.java | 51 +++++- .../api/PrudentialFloorsApiResource.java | 63 +++++++ .../PrudentialFloorReadPlatformService.java | 63 +++++++ .../db/changelog/tenant/changelog-tenant.xml | 7 + ...0598_add_loan_product_sma_start_fields.xml | 39 ++++ .../0599_add_delinquency_range_stage.xml | 35 ++++ ...able_loan_product_update_maker_checker.xml | 45 +++++ .../0600_create_prudential_floor_master.xml | 167 ++++++++++++++++++ ...an_product_advance_ext_sicr_lgd_fields.xml | 49 +++++ ...ntDelinquencyRangeEventSerializerTest.java | 4 +- ...tePlatformServiceRangeChangeEventTest.java | 28 +-- 28 files changed, 874 insertions(+), 32 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/data/PrudentialFloorData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/domain/PrudentialFloor.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/domain/PrudentialFloorRepository.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/mapper/PrudentialFloorMapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/api/PrudentialFloorsApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/service/PrudentialFloorReadPlatformService.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0598_add_loan_product_sma_start_fields.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_add_delinquency_range_stage.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_enable_loan_product_update_maker_checker.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0600_create_prudential_floor_master.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0601_add_loan_product_advance_ext_sicr_lgd_fields.xml diff --git a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java index d32c6b0eb..a52596980 100644 --- a/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java +++ b/fineract-accounting/src/main/java/org/apache/fineract/accounting/producttoaccountmapping/serialization/ProductToGLAccountMappingFromApiJsonDeserializer.java @@ -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(); } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java index 00d573ffe..fcbf8a932 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRange.java @@ -46,17 +46,27 @@ public class DelinquencyRange extends AbstractAuditableWithUTCDateTimeCustom { + + @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; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/domain/PrudentialFloorRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/domain/PrudentialFloorRepository.java new file mode 100644 index 000000000..e67314205 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/domain/PrudentialFloorRepository.java @@ -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 { + + List findByProductCategoryAndStage(String productCategory, Integer stage); + + List findByProductCategoryOrderByStageAscDurationFromDaysAsc(String productCategory); +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/mapper/PrudentialFloorMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/mapper/PrudentialFloorMapper.java new file mode 100644 index 000000000..cd060a5ca --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/prudentialfloor/mapper/PrudentialFloorMapper.java @@ -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 map(List sources); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java index 951aacd5c..3c75d2173 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/api/MakercheckersApiResource.java @@ -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 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 }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformService.java index 059f40d24..ba632d1e8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformService.java @@ -33,6 +33,9 @@ public interface AuditReadPlatformService { List retrieveAllEntriesToBeChecked(SQLBuilder extraCriteria, boolean includeJson); + /** Requests the current user submitted that still await a checker, whatever the user's own checker permissions. */ + List retrieveMyPendingEntries(SQLBuilder extraCriteria, boolean includeJson); + AuditData retrieveAuditEntry(Long auditId); AuditSearchData retrieveSearchTemplate(String useType); diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java index 40ce1284b..7a4455db7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java @@ -214,10 +214,17 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService { return retrieveEntries("makerchecker", extraCriteria, " order by aud.id, mk.username", includeJson); } + @Override + public List 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 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); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 57a683d1d..310f4d1ed 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java index bc233bc74..cdc8465e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductAdvanceExtService.java @@ -43,7 +43,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 +77,11 @@ 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" }; /** * Insert or partial-update the satellite for this product. Returns the fields that actually changed so the @@ -155,6 +160,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); @@ -289,6 +302,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 +430,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()); @@ -509,6 +543,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; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/api/PrudentialFloorsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/api/PrudentialFloorsApiResource.java new file mode 100644 index 000000000..fa0aae59e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/api/PrudentialFloorsApiResource.java @@ -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 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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/service/PrudentialFloorReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/service/PrudentialFloorReadPlatformService.java new file mode 100644 index 000000000..0e200bc69 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/prudentialfloor/service/PrudentialFloorReadPlatformService.java @@ -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 retrieveAll() { + return mapper.map(repository.findAll()); + } + + public List 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 candidates = repository.findByProductCategoryAndStage(productCategory, stage); + return candidates.stream().filter(row -> row.coversDuration(daysInStage)).findFirst().map(mapper::map).orElse(null); + } +} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 5d468f7d2..04282767c 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -623,6 +623,13 @@ + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0598_add_loan_product_sma_start_fields.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0598_add_loan_product_sma_start_fields.xml new file mode 100644 index 000000000..d5fd087f9 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0598_add_loan_product_sma_start_fields.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_add_delinquency_range_stage.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_add_delinquency_range_stage.xml new file mode 100644 index 000000000..e44eff9fd --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_add_delinquency_range_stage.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_enable_loan_product_update_maker_checker.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_enable_loan_product_update_maker_checker.xml new file mode 100644 index 000000000..1d034c28e --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0599_enable_loan_product_update_maker_checker.xml @@ -0,0 +1,45 @@ + + + + + + + + ANY + + UPDATE m_permission + SET can_maker_checker = TRUE + WHERE code = 'UPDATE_LOANPRODUCT'; + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0600_create_prudential_floor_master.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0600_create_prudential_floor_master.xml new file mode 100644 index 000000000..8e6eeb4fb --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0600_create_prudential_floor_master.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0601_add_loan_product_advance_ext_sicr_lgd_fields.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0601_add_loan_product_advance_ext_sicr_lgd_fields.xml new file mode 100644 index 000000000..9b1187f59 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0601_add_loan_product_advance_ext_sicr_lgd_fields.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java index 5b7405027..a1feba4f3 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanAccountDelinquencyRangeEventSerializerTest.java @@ -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); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index d692d1d96..c6668af2f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -174,9 +174,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { final List 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 listDelinquencyRanges = Arrays.asList(range1, range2); DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket"); @@ -217,9 +217,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { final List 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 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 listDelinquencyRanges = Arrays.asList(range1, range2); @@ -346,9 +346,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { final List 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 listDelinquencyRanges = Arrays.asList(range1, range2); DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket"); @@ -425,9 +425,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { final List 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 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 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 listDelinquencyRanges = Arrays.asList(range1, range2); DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");