Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a320b9c09 | ||
|
|
8ba0420398 |
+2
-2
@@ -113,7 +113,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
|
|||||||
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
|
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
|
||||||
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue())
|
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue())
|
||||||
.value(incomeFromRecoveryAccountId).ignoreIfNull().integerGreaterThanZero();
|
.value(incomeFromRecoveryAccountId).notNull().integerGreaterThanZero();
|
||||||
|
|
||||||
final Long writeOffAccountId = this.fromApiJsonHelper
|
final Long writeOffAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
|
||||||
@@ -128,7 +128,7 @@ public final class ProductToGLAccountMappingFromApiJsonDeserializer {
|
|||||||
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
||||||
baseDataValidator.reset().parameter(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
baseDataValidator.reset().parameter(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
||||||
.value(transfersInSuspenseAccountId).ignoreIfNull().integerGreaterThanZero();
|
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-13
@@ -46,27 +46,17 @@ public class DelinquencyRange extends AbstractAuditableWithUTCDateTimeCustom<Lon
|
|||||||
@Column(name = "max_age_days", nullable = true)
|
@Column(name = "max_age_days", nullable = true)
|
||||||
private Integer maximumAgeDays;
|
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
|
@Version
|
||||||
private Long version;
|
private Long version;
|
||||||
|
|
||||||
protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays, Integer stage) {
|
protected DelinquencyRange(@NotNull String classification, @NotNull Integer minimumAgeDays, Integer maximumAgeDays) {
|
||||||
this.classification = classification;
|
this.classification = classification;
|
||||||
this.minimumAgeDays = minimumAgeDays;
|
this.minimumAgeDays = minimumAgeDays;
|
||||||
this.maximumAgeDays = maximumAgeDays;
|
this.maximumAgeDays = maximumAgeDays;
|
||||||
this.stage = stage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge,
|
public static DelinquencyRange instance(@NotNull String classification, @NotNull Integer minimumAge, Integer maximumAge) {
|
||||||
Integer stage) {
|
return new DelinquencyRange(classification, minimumAge, maximumAge);
|
||||||
return new DelinquencyRange(classification, minimumAge, maximumAge, stage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-17
@@ -53,20 +53,12 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
paymentTypeRepositoryWrapper, codeValueRepository);
|
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
|
* Set of abstractions for saving Loan Products to GL Account Mappings
|
||||||
***/
|
***/
|
||||||
|
|
||||||
public void saveLoanToAssetAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
public void saveLoanToAssetAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
||||||
final int placeHolderTypeId) {
|
final int placeHolderTypeId) {
|
||||||
if (!isMapped(element, paramName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.ASSET, PortfolioProductType.LOAN);
|
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.ASSET, PortfolioProductType.LOAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,25 +72,16 @@ public class LoanProductToGLAccountMappingHelper extends ProductToGLAccountMappi
|
|||||||
|
|
||||||
public void saveLoanToIncomeAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
public void saveLoanToIncomeAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
||||||
final int placeHolderTypeId) {
|
final int placeHolderTypeId) {
|
||||||
if (!isMapped(element, paramName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.INCOME, PortfolioProductType.LOAN);
|
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.INCOME, PortfolioProductType.LOAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveLoanToExpenseAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
public void saveLoanToExpenseAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
||||||
final int placeHolderTypeId) {
|
final int placeHolderTypeId) {
|
||||||
if (!isMapped(element, paramName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.EXPENSE, PortfolioProductType.LOAN);
|
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.EXPENSE, PortfolioProductType.LOAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveLoanToLiabilityAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
public void saveLoanToLiabilityAccountMapping(final JsonElement element, final String paramName, final Long productId,
|
||||||
final int placeHolderTypeId) {
|
final int placeHolderTypeId) {
|
||||||
if (!isMapped(element, paramName)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.LIABILITY, PortfolioProductType.LOAN);
|
saveProductToAccountMapping(element, paramName, productId, placeHolderTypeId, GLAccountType.LIABILITY, PortfolioProductType.LOAN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-1
@@ -29,7 +29,6 @@ public final class DelinquencyApiConstants {
|
|||||||
public static final String RANGES_PARAM_NAME = "ranges";
|
public static final String RANGES_PARAM_NAME = "ranges";
|
||||||
public static final String MINIMUMAGEDAYS_PARAM_NAME = "minimumAgeDays";
|
public static final String MINIMUMAGEDAYS_PARAM_NAME = "minimumAgeDays";
|
||||||
public static final String MAXIMUMAGEDAYS_PARAM_NAME = "maximumAgeDays";
|
public static final String MAXIMUMAGEDAYS_PARAM_NAME = "maximumAgeDays";
|
||||||
public static final String STAGE_PARAM_NAME = "stage";
|
|
||||||
public static final String LOCALE_PARAM_NAME = "locale";
|
public static final String LOCALE_PARAM_NAME = "locale";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -35,6 +35,5 @@ public class DelinquencyRangeRequest implements Serializable {
|
|||||||
private String classification;
|
private String classification;
|
||||||
private Integer minimumAgeDays;
|
private Integer minimumAgeDays;
|
||||||
private Integer maximumAgeDays;
|
private Integer maximumAgeDays;
|
||||||
private Integer stage;
|
|
||||||
private String locale;
|
private String locale;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -34,15 +34,13 @@ public class DelinquencyRangeData implements Serializable {
|
|||||||
private String classification;
|
private String classification;
|
||||||
private Integer minimumAgeDays;
|
private Integer minimumAgeDays;
|
||||||
private Integer maximumAgeDays;
|
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, Integer stage) {
|
public static DelinquencyRangeData instance(String classification, Integer minimumAgeDays, Integer maximumAgeDays) {
|
||||||
return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays, stage);
|
return new DelinquencyRangeData(null, classification, minimumAgeDays, maximumAgeDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DelinquencyRangeData reference(Long id) {
|
public static DelinquencyRangeData reference(Long id) {
|
||||||
return new DelinquencyRangeData(id, "", 0, 0, null);
|
return new DelinquencyRangeData(id, "", 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -305,7 +305,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays());
|
throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays());
|
||||||
}
|
}
|
||||||
DelinquencyRange newDelinquencyRange = DelinquencyRange.instance(data.getClassification(), data.getMinimumAgeDays(),
|
DelinquencyRange newDelinquencyRange = DelinquencyRange.instance(data.getClassification(), data.getMinimumAgeDays(),
|
||||||
data.getMaximumAgeDays(), data.getStage());
|
data.getMaximumAgeDays());
|
||||||
return repositoryRange.saveAndFlush(newDelinquencyRange);
|
return repositoryRange.saveAndFlush(newDelinquencyRange);
|
||||||
} else {
|
} else {
|
||||||
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated",
|
throw new PlatformDataIntegrityException("error.msg.data.integrity.issue.entity.duplicated",
|
||||||
@@ -327,10 +327,6 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
delinquencyRange.setMaximumAgeDays(data.getMaximumAgeDays());
|
delinquencyRange.setMaximumAgeDays(data.getMaximumAgeDays());
|
||||||
changes.put(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, 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()) {
|
if (!changes.isEmpty()) {
|
||||||
delinquencyRange = repositoryRange.saveAndFlush(delinquencyRange);
|
delinquencyRange = repositoryRange.saveAndFlush(delinquencyRange);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-5
@@ -56,8 +56,7 @@ public class DelinquencyRangeParseAndValidator extends ParseAndValidator {
|
|||||||
|
|
||||||
jsonHelper.checkForUnsupportedParameters(element,
|
jsonHelper.checkForUnsupportedParameters(element,
|
||||||
List.of(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME,
|
List.of(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME,
|
||||||
DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.STAGE_PARAM_NAME,
|
DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME, DelinquencyApiConstants.LOCALE_PARAM_NAME));
|
||||||
DelinquencyApiConstants.LOCALE_PARAM_NAME));
|
|
||||||
|
|
||||||
final String localeValue = jsonHelper.extractStringNamed(DelinquencyApiConstants.LOCALE_PARAM_NAME, element);
|
final String localeValue = jsonHelper.extractStringNamed(DelinquencyApiConstants.LOCALE_PARAM_NAME, element);
|
||||||
dataValidator.reset().parameter(DelinquencyApiConstants.LOCALE_PARAM_NAME).value(localeValue).notBlank();
|
dataValidator.reset().parameter(DelinquencyApiConstants.LOCALE_PARAM_NAME).value(localeValue).notBlank();
|
||||||
@@ -66,15 +65,13 @@ public class DelinquencyRangeParseAndValidator extends ParseAndValidator {
|
|||||||
final String classification = jsonHelper.extractStringNamed(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, element);
|
final String classification = jsonHelper.extractStringNamed(DelinquencyApiConstants.CLASSIFICATION_PARAM_NAME, element);
|
||||||
final Integer minimumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, element, locale);
|
final Integer minimumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME, element, locale);
|
||||||
final Integer maximumAge = jsonHelper.extractIntegerNamed(DelinquencyApiConstants.MAXIMUMAGEDAYS_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.CLASSIFICATION_PARAM_NAME).value(classification).notBlank();
|
||||||
dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME).value(minimumAge).notBlank()
|
dataValidator.reset().parameter(DelinquencyApiConstants.MINIMUMAGEDAYS_PARAM_NAME).value(minimumAge).notBlank()
|
||||||
.integerGreaterThanNumber(0);
|
.integerGreaterThanNumber(0);
|
||||||
dataValidator.reset().parameter(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME).value(maximumAge).ignoreIfNull()
|
dataValidator.reset().parameter(DelinquencyApiConstants.MAXIMUMAGEDAYS_PARAM_NAME).value(maximumAge).ignoreIfNull()
|
||||||
.integerGreaterThanNumber(0);
|
.integerGreaterThanNumber(0);
|
||||||
dataValidator.reset().parameter(DelinquencyApiConstants.STAGE_PARAM_NAME).value(stage).ignoreIfNull().inMinMaxRange(1, 3);
|
|
||||||
|
|
||||||
return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge, stage);
|
return dataValidator.hasError() ? null : DelinquencyRangeData.instance(classification, minimumAge, maximumAge);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-172
@@ -137,141 +137,6 @@ public class LoanProductAdvanceExt {
|
|||||||
@Column(name = "npa_trigger")
|
@Column(name = "npa_trigger")
|
||||||
private String npaTrigger;
|
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")
|
@Column(name = "interest_after_npa")
|
||||||
private String interestAfterNpa;
|
private String interestAfterNpa;
|
||||||
|
|
||||||
@@ -599,43 +464,6 @@ public class LoanProductAdvanceExt {
|
|||||||
@Column(name = "loan_balance_transfer_out_allowed")
|
@Column(name = "loan_balance_transfer_out_allowed")
|
||||||
private Boolean balanceTransferOutAllowed;
|
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) {
|
public LoanProductAdvanceExt(final LoanProduct loanProduct) {
|
||||||
this.loanProduct = loanProduct;
|
this.loanProduct = loanProduct;
|
||||||
this.loanProductId = loanProduct.getId();
|
this.loanProductId = loanProduct.getId();
|
||||||
|
|||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
-11
@@ -79,17 +79,6 @@ 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
|
@GET
|
||||||
@Path("/searchtemplate")
|
@Path("/searchtemplate")
|
||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
|||||||
-3
@@ -33,9 +33,6 @@ public interface AuditReadPlatformService {
|
|||||||
|
|
||||||
List<AuditData> retrieveAllEntriesToBeChecked(SQLBuilder extraCriteria, boolean includeJson);
|
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);
|
AuditData retrieveAuditEntry(Long auditId);
|
||||||
|
|
||||||
AuditSearchData retrieveSearchTemplate(String useType);
|
AuditSearchData retrieveSearchTemplate(String useType);
|
||||||
|
|||||||
+1
-8
@@ -214,17 +214,10 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
|
|||||||
return retrieveEntries("makerchecker", extraCriteria, " order by aud.id, mk.username", includeJson);
|
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,
|
private List<AuditData> retrieveEntries(final String useType, final SQLBuilder extraCriteria, final String groupAndOrderBySQL,
|
||||||
final boolean includeJson) {
|
final boolean includeJson) {
|
||||||
|
|
||||||
if ((!useType.equals("audit") && !useType.equals("makerchecker") && !useType.equals("makerchecker-mine"))) {
|
if ((!useType.equals("audit") && !useType.equals("makerchecker"))) {
|
||||||
throw new PlatformDataIntegrityException("error.msg.invalid.auditSearchTemplate.useType",
|
throw new PlatformDataIntegrityException("error.msg.invalid.auditSearchTemplate.useType",
|
||||||
"Invalid Audit Search Template UseType: " + useType);
|
"Invalid Audit Search Template UseType: " + useType);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -56,6 +56,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -98,6 +99,7 @@ public class CashierDepositRequestApiResource {
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final VoucherService voucherService;
|
private final VoucherService voucherService;
|
||||||
|
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
// ── POST /v1/cashier/deposit-requests ── Cashier submits new deposit request ──
|
// ── POST /v1/cashier/deposit-requests ── Cashier submits new deposit request ──
|
||||||
@@ -155,6 +157,11 @@ public class CashierDepositRequestApiResource {
|
|||||||
throw apiError(400, "Amount must be greater than zero");
|
throw apiError(400, "Amount must be greater than zero");
|
||||||
}
|
}
|
||||||
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.CREDIT);
|
kycRestrictionGuard.check(savingsAccountId, KycRestrictionGuard.Operation.CREDIT);
|
||||||
|
// An FD is funded once, with exactly its principal. Told here at the counter rather than when the
|
||||||
|
// manager approves; the FD deposit command checks the same again at posting.
|
||||||
|
if (!isGlType && "FIXED_DEPOSIT".equals(resolveDepositAccountCategory(savingsAccountId, accountNo))) {
|
||||||
|
fixedDepositFundingValidator.validateFirstFunding(savingsAccountId, amount);
|
||||||
|
}
|
||||||
validateDenominationTotal(denominationData, amount);
|
validateDenominationTotal(denominationData, amount);
|
||||||
validateDdLimit(ddFaceValue);
|
validateDdLimit(ddFaceValue);
|
||||||
|
|
||||||
|
|||||||
+81
@@ -69,6 +69,8 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
|||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
@@ -90,6 +92,8 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final FineractRequestContextHolder fineractRequestContextHolder;
|
private final FineractRequestContextHolder fineractRequestContextHolder;
|
||||||
private final VoucherService voucherService;
|
private final VoucherService voucherService;
|
||||||
|
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||||
|
private final DepositAccountWritePlatformService depositAccountWritePlatformService;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
// ── POST /v1/clerk/disbursement-requests ── Clerk submits new request ────────
|
// ── POST /v1/clerk/disbursement-requests ── Clerk submits new request ────────
|
||||||
@@ -202,6 +206,9 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixed deposit legs: credit-only, funded once with exactly the principal, from the customer's own account.
|
||||||
|
validateFixedDepositLegs(creditEntries, requestType);
|
||||||
|
|
||||||
// Validate balance when both DR and CR entries present (collection mode)
|
// Validate balance when both DR and CR entries present (collection mode)
|
||||||
if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0
|
if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0
|
||||||
&& totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) {
|
&& totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) {
|
||||||
@@ -491,6 +498,10 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
rejectRecurringDepositLeg(entry, entryType);
|
rejectRecurringDepositLeg(entry, entryType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Re-checked here, not only at submit: another request (or a counter deposit) may have funded the
|
||||||
|
// same FD while this one waited, and a refusal now still leaves the request PENDING.
|
||||||
|
final Set<Long> fundedFixedDeposits = validateFixedDepositLegs(creditEntries,
|
||||||
|
record.get("request_type") != null ? (String) record.get("request_type") : "DISBURSEMENT");
|
||||||
|
|
||||||
// Atomic PENDING → PROCESSING lock: prevents two managers from concurrently
|
// Atomic PENDING → PROCESSING lock: prevents two managers from concurrently
|
||||||
// posting duplicate Fineract transactions for the same request.
|
// posting duplicate Fineract transactions for the same request.
|
||||||
@@ -773,6 +784,12 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
}
|
}
|
||||||
} // end else (disbursement/collection mode)
|
} // end else (disbursement/collection mode)
|
||||||
|
|
||||||
|
// A funded FD's maturity date and amount were projected off its principal with no money in it;
|
||||||
|
// the direct credit above does not recompute them, so do it now that the money has landed.
|
||||||
|
for (final Long fdId : fundedFixedDeposits) {
|
||||||
|
depositAccountWritePlatformService.recalculateFixedDepositMaturity(fdId);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Compensate: undo all posted Fineract transactions in reverse order (best-effort).
|
// Compensate: undo all posted Fineract transactions in reverse order (best-effort).
|
||||||
// This restores account balances even when a later entry fails mid-processing.
|
// This restores account balances even when a later entry fails mid-processing.
|
||||||
@@ -1223,6 +1240,70 @@ public class ClerkDisbursementRequestApiResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed deposit legs of a clerk request, recognised by id (deposit_type_enum) rather than the entry's label.
|
||||||
|
* <ul>
|
||||||
|
* <li>An FD is never debited here: money leaves an FD only through its maturity or premature closure. The
|
||||||
|
* direct book debit used by Bank Transfer and Collection would otherwise bypass the FD's own refusal.</li>
|
||||||
|
* <li>An FD credit is its first and only funding, for exactly its principal, and an FD appears once.</li>
|
||||||
|
* <li>The FDs funded by one request belong to one customer, and every Savings/Current account debited to fund
|
||||||
|
* them belongs to that customer (or is held jointly) and can pay its amount. A GL source (Disbursement) is
|
||||||
|
* the bank's own money and has no customer to compare.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return the ids of the FDs this request funds, for the maturity recalculation after posting
|
||||||
|
*/
|
||||||
|
private Set<Long> validateFixedDepositLegs(final JsonArray entries, final String requestType) {
|
||||||
|
final Set<Long> fdIds = new LinkedHashSet<>();
|
||||||
|
Long firstFdId = null;
|
||||||
|
Long fdClientId = null;
|
||||||
|
for (final JsonElement el : entries) {
|
||||||
|
final JsonObject entry = el.getAsJsonObject();
|
||||||
|
final String type = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
|
||||||
|
if ("GL".equals(type) || "CURRENT".equals(type) || !entry.has("savingsAccountId")
|
||||||
|
|| entry.get("savingsAccountId").isJsonNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Long accountId = entry.get("savingsAccountId").getAsLong();
|
||||||
|
if (!fixedDepositFundingValidator.isFixedDeposit(accountId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : String.valueOf(accountId);
|
||||||
|
if ("Dr".equals(entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr")) {
|
||||||
|
throw apiError(422, "Fixed deposit " + acNo + " cannot be debited. Money leaves a fixed deposit only"
|
||||||
|
+ " through its maturity or premature closure.");
|
||||||
|
}
|
||||||
|
if (!fdIds.add(accountId)) {
|
||||||
|
throw apiError(422, "Fixed deposit " + acNo + " appears more than once. A fixed deposit is funded once,"
|
||||||
|
+ " with its full principal in one entry.");
|
||||||
|
}
|
||||||
|
final BigDecimal amount = parseBigDecimal(entry.has("amount") ? entry.get("amount").getAsString() : null);
|
||||||
|
final Long clientId = fixedDepositFundingValidator.validateFirstFunding(accountId, amount);
|
||||||
|
if (firstFdId == null) {
|
||||||
|
firstFdId = accountId;
|
||||||
|
fdClientId = clientId;
|
||||||
|
} else if (!Objects.equals(fdClientId, clientId)) {
|
||||||
|
throw apiError(422, "One " + voucherTypeLabelFor(requestType) + " can fund fixed deposits of one customer"
|
||||||
|
+ " only. Raise a separate request for each customer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fdIds.isEmpty()) {
|
||||||
|
return fdIds;
|
||||||
|
}
|
||||||
|
for (final JsonElement el : entries) {
|
||||||
|
final JsonObject entry = el.getAsJsonObject();
|
||||||
|
final String type = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS";
|
||||||
|
if (!"Dr".equals(entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr") || "GL".equals(type)
|
||||||
|
|| !entry.has("savingsAccountId") || entry.get("savingsAccountId").isJsonNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final BigDecimal amount = parseBigDecimal(entry.has("amount") ? entry.get("amount").getAsString() : null);
|
||||||
|
fixedDepositFundingValidator.validateFundingSource(firstFdId, fdClientId, entry.get("savingsAccountId").getAsLong(),
|
||||||
|
"CURRENT".equals(type), amount);
|
||||||
|
}
|
||||||
|
return fdIds;
|
||||||
|
}
|
||||||
|
|
||||||
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
|
/** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */
|
||||||
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
private static final int SAVINGS_STATUS_ACTIVE = 300;
|
||||||
|
|
||||||
|
|||||||
+8
@@ -57,6 +57,7 @@ import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
|||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
import org.apache.fineract.portfolio.client.domain.KycExpiryStage;
|
||||||
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
import org.apache.fineract.portfolio.client.service.KycRestrictionGuard;
|
||||||
|
import org.apache.fineract.portfolio.savings.service.FixedDepositFundingValidator;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
import org.apache.fineract.portfolio.voucher.service.VoucherLine;
|
||||||
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
import org.apache.fineract.portfolio.voucher.service.VoucherService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -85,6 +86,7 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final VoucherService voucherService;
|
private final VoucherService voucherService;
|
||||||
|
private final FixedDepositFundingValidator fixedDepositFundingValidator;
|
||||||
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
private final Gson gson = new GsonBuilder().serializeNulls().create();
|
||||||
|
|
||||||
// ── POST /v1/clerk/withdrawal-requests ── Clerk submits new request ─────────
|
// ── POST /v1/clerk/withdrawal-requests ── Clerk submits new request ─────────
|
||||||
@@ -127,6 +129,12 @@ public class ClerkWithdrawalRequestApiResource {
|
|||||||
} else if (savingsAccountId == null) {
|
} else if (savingsAccountId == null) {
|
||||||
throw apiError(400, "Savings account id is required for a withdrawal request");
|
throw apiError(400, "Savings account id is required for a withdrawal request");
|
||||||
}
|
}
|
||||||
|
// Money leaves a fixed deposit only through its maturity or premature closure, never a withdrawal slip.
|
||||||
|
// Checked by id: the slip's own label is not trusted, and a CURRENT id belongs to m_current_account.
|
||||||
|
if (!isGl && !"CURRENT".equalsIgnoreCase(accountType) && fixedDepositFundingValidator.isFixedDeposit(savingsAccountId)) {
|
||||||
|
throw apiError(422, "Account " + accountNo + " is a fixed deposit and cannot be withdrawn from."
|
||||||
|
+ " Money leaves a fixed deposit only through its maturity or premature closure.");
|
||||||
|
}
|
||||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
throw apiError(400, "Amount must be greater than zero");
|
throw apiError(400, "Amount must be greater than zero");
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -679,9 +679,8 @@ public final class LoanProductDataValidator {
|
|||||||
|
|
||||||
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
final Long transfersInSuspenseAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue(), element);
|
||||||
// not mapped by the bank (only the nine GLs are); optional
|
|
||||||
baseDataValidator.reset().parameter(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
baseDataValidator.reset().parameter(LoanProductAccountingParams.TRANSFERS_SUSPENSE.getValue())
|
||||||
.value(transfersInSuspenseAccountId).ignoreIfNull().integerGreaterThanZero();
|
.value(transfersInSuspenseAccountId).notNull().integerGreaterThanZero();
|
||||||
|
|
||||||
final Long incomeFromInterestId = this.fromApiJsonHelper
|
final Long incomeFromInterestId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.INTEREST_ON_LOANS.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.INTEREST_ON_LOANS.getValue(), element);
|
||||||
@@ -701,7 +700,7 @@ public final class LoanProductDataValidator {
|
|||||||
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
|
final Long incomeFromRecoveryAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue(), element);
|
||||||
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue())
|
baseDataValidator.reset().parameter(LoanProductAccountingParams.INCOME_FROM_RECOVERY.getValue())
|
||||||
.value(incomeFromRecoveryAccountId).ignoreIfNull().integerGreaterThanZero();
|
.value(incomeFromRecoveryAccountId).notNull().integerGreaterThanZero();
|
||||||
|
|
||||||
final Long writeOffAccountId = this.fromApiJsonHelper
|
final Long writeOffAccountId = this.fromApiJsonHelper
|
||||||
.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
|
.extractLongNamed(LoanProductAccountingParams.LOSSES_WRITTEN_OFF.getValue(), element);
|
||||||
|
|||||||
+2
-332
@@ -9,15 +9,11 @@ package org.apache.fineract.portfolio.loanproduct.service;
|
|||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
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.LoanProduct;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExt;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExt;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExtRepository;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductAdvanceExtRepository;
|
||||||
@@ -47,8 +43,7 @@ public class LoanProductAdvanceExtService {
|
|||||||
"securityType", "chargeType", "eligibleAmountBasis", "margin", "ltv", "dpMethod", "lienRequired",
|
"securityType", "chargeType", "eligibleAmountBasis", "margin", "ltv", "dpMethod", "lienRequired",
|
||||||
"lienMarkingStage", "lienCertificateRequired", "lienCertificatePrefix", "printCertificateOnDisbursement",
|
"lienMarkingStage", "lienCertificateRequired", "lienCertificatePrefix", "printCertificateOnDisbursement",
|
||||||
"interestBasis", "spread", "interestChargedOn", "interestPostingFrequency", "penalApplicable", "penalRate",
|
"interestBasis", "spread", "interestChargedOn", "interestPostingFrequency", "penalApplicable", "penalRate",
|
||||||
"penalAppliedOn", "penalGraceDays", "npaTrigger", "smaStartAfterMonths", "stockStatementValidityDays", "memberRequired", "guarantorMustBeMember",
|
"penalAppliedOn", "penalGraceDays", "npaTrigger", "interestAfterNpa", "undebitedInterestGlId",
|
||||||
"incomeMultiple", "minVintageMonths", "requiredDocuments", "interestAfterNpa", "undebitedInterestGlId",
|
|
||||||
"renewalAllowed", "renewalPeriodMonths", "renewalGraceDays", "preclosureAllowed", "lockInMonths",
|
"renewalAllowed", "renewalPeriodMonths", "renewalGraceDays", "preclosureAllowed", "lockInMonths",
|
||||||
"interestOnPreclosure", "revaluationFreqMonths", "insuranceRequired", "stockStatementFreqMonths",
|
"interestOnPreclosure", "revaluationFreqMonths", "insuranceRequired", "stockStatementFreqMonths",
|
||||||
"chequeBookEnabled", "chequeType", "defaultChequeBookSize", "ppsThreshold", "ppsMandatoryAbove",
|
"chequeBookEnabled", "chequeType", "defaultChequeBookSize", "ppsThreshold", "ppsMandatoryAbove",
|
||||||
@@ -81,17 +76,7 @@ public class LoanProductAdvanceExtService {
|
|||||||
"partPrepaymentAllowed", "minPartPayment", "allowedOn", "partPaymentsPerYear",
|
"partPrepaymentAllowed", "minPartPayment", "allowedOn", "partPaymentsPerYear",
|
||||||
"partPrepaymentMode", "foreclosureFloatingIndividual", "foreclosureFixedIndividual", "foreclosureNonIndividual",
|
"partPrepaymentMode", "foreclosureFloatingIndividual", "foreclosureFixedIndividual", "foreclosureNonIndividual",
|
||||||
"foreclosureTakeover", "foreclosureQuoteValidDays", "autoCloseOnZeroBalance", "nocLetter",
|
"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
|
* Insert or partial-update the satellite for this product. Returns the fields that actually changed so the
|
||||||
@@ -105,205 +90,10 @@ public class LoanProductAdvanceExtService {
|
|||||||
final LoanProductAdvanceExt ext = this.repository.findByLoanProductId(loanProduct.getId())
|
final LoanProductAdvanceExt ext = this.repository.findByLoanProductId(loanProduct.getId())
|
||||||
.orElseGet(() -> new LoanProductAdvanceExt(loanProduct));
|
.orElseGet(() -> new LoanProductAdvanceExt(loanProduct));
|
||||||
applyScalarFields(ext, command, changes);
|
applyScalarFields(ext, command, changes);
|
||||||
validateGold(ext, command);
|
|
||||||
validateWarehouse(ext, command);
|
|
||||||
this.repository.save(ext);
|
this.repository.save(ext);
|
||||||
return changes;
|
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) {
|
public Map<String, Object> retrieveAsMap(final Long loanProductId) {
|
||||||
return this.repository.findByLoanProductId(loanProductId).map(LoanProductAdvanceExtService::toMap)
|
return this.repository.findByLoanProductId(loanProductId).map(LoanProductAdvanceExtService::toMap)
|
||||||
.orElseGet(LinkedHashMap::new);
|
.orElseGet(LinkedHashMap::new);
|
||||||
@@ -365,14 +155,6 @@ public class LoanProductAdvanceExtService {
|
|||||||
setString(command, "penalAppliedOn", ext::getPenalAppliedOn, ext::setPenalAppliedOn, changes);
|
setString(command, "penalAppliedOn", ext::getPenalAppliedOn, ext::setPenalAppliedOn, changes);
|
||||||
setInteger(command, "penalGraceDays", ext::getPenalGraceDays, ext::setPenalGraceDays, changes);
|
setInteger(command, "penalGraceDays", ext::getPenalGraceDays, ext::setPenalGraceDays, changes);
|
||||||
setString(command, "npaTrigger", ext::getNpaTrigger, ext::setNpaTrigger, 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);
|
setString(command, "interestAfterNpa", ext::getInterestAfterNpa, ext::setInterestAfterNpa, changes);
|
||||||
setLong(command, "undebitedInterestGlId", ext::getUndebitedInterestGlId, ext::setUndebitedInterestGlId, changes);
|
setLong(command, "undebitedInterestGlId", ext::getUndebitedInterestGlId, ext::setUndebitedInterestGlId, changes);
|
||||||
setBoolean(command, "renewalAllowed", ext::getRenewalAllowed, ext::setRenewalAllowed, changes);
|
setBoolean(command, "renewalAllowed", ext::getRenewalAllowed, ext::setRenewalAllowed, changes);
|
||||||
@@ -381,48 +163,6 @@ public class LoanProductAdvanceExtService {
|
|||||||
setString(command, "preclosureAllowed", ext::getPreclosureAllowed, ext::setPreclosureAllowed, changes);
|
setString(command, "preclosureAllowed", ext::getPreclosureAllowed, ext::setPreclosureAllowed, changes);
|
||||||
setInteger(command, "lockInMonths", ext::getLockInMonths, ext::setLockInMonths, changes);
|
setInteger(command, "lockInMonths", ext::getLockInMonths, ext::setLockInMonths, changes);
|
||||||
setString(command, "interestOnPreclosure", ext::getInterestOnPreclosure, ext::setInterestOnPreclosure, 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,
|
setInteger(command, "revaluationFreqMonths", ext::getRevaluationFreqMonths, ext::setRevaluationFreqMonths,
|
||||||
changes);
|
changes);
|
||||||
setBoolean(command, "insuranceRequired", ext::getInsuranceRequired, ext::setInsuranceRequired, changes);
|
setBoolean(command, "insuranceRequired", ext::getInsuranceRequired, ext::setInsuranceRequired, changes);
|
||||||
@@ -549,20 +289,6 @@ public class LoanProductAdvanceExtService {
|
|||||||
setBoolean(command, "releaseCollateralOnClosure", ext::getReleaseCollateralOnClosure, ext::setReleaseCollateralOnClosure, changes);
|
setBoolean(command, "releaseCollateralOnClosure", ext::getReleaseCollateralOnClosure, ext::setReleaseCollateralOnClosure, changes);
|
||||||
setBoolean(command, "topUpAllowed", ext::getTopUpAllowed, ext::setTopUpAllowed, changes);
|
setBoolean(command, "topUpAllowed", ext::getTopUpAllowed, ext::setTopUpAllowed, changes);
|
||||||
setBoolean(command, "balanceTransferOutAllowed", ext::getBalanceTransferOutAllowed, ext::setBalanceTransferOutAllowed, 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 ----------
|
// ---- typed setters: only write params that are present, and only report real changes ----------
|
||||||
@@ -677,13 +403,6 @@ public class LoanProductAdvanceExtService {
|
|||||||
map.put("penalAppliedOn", ext.getPenalAppliedOn());
|
map.put("penalAppliedOn", ext.getPenalAppliedOn());
|
||||||
map.put("penalGraceDays", ext.getPenalGraceDays());
|
map.put("penalGraceDays", ext.getPenalGraceDays());
|
||||||
map.put("npaTrigger", ext.getNpaTrigger());
|
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("interestAfterNpa", ext.getInterestAfterNpa());
|
||||||
map.put("undebitedInterestGlId", ext.getUndebitedInterestGlId());
|
map.put("undebitedInterestGlId", ext.getUndebitedInterestGlId());
|
||||||
map.put("renewalAllowed", ext.getRenewalAllowed());
|
map.put("renewalAllowed", ext.getRenewalAllowed());
|
||||||
@@ -692,42 +411,6 @@ public class LoanProductAdvanceExtService {
|
|||||||
map.put("preclosureAllowed", ext.getPreclosureAllowed());
|
map.put("preclosureAllowed", ext.getPreclosureAllowed());
|
||||||
map.put("lockInMonths", ext.getLockInMonths());
|
map.put("lockInMonths", ext.getLockInMonths());
|
||||||
map.put("interestOnPreclosure", ext.getInterestOnPreclosure());
|
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("revaluationFreqMonths", ext.getRevaluationFreqMonths());
|
||||||
map.put("insuranceRequired", ext.getInsuranceRequired());
|
map.put("insuranceRequired", ext.getInsuranceRequired());
|
||||||
map.put("stockStatementFreqMonths", ext.getStockStatementFreqMonths());
|
map.put("stockStatementFreqMonths", ext.getStockStatementFreqMonths());
|
||||||
@@ -826,19 +509,6 @@ public class LoanProductAdvanceExtService {
|
|||||||
map.put("releaseCollateralOnClosure", ext.getReleaseCollateralOnClosure());
|
map.put("releaseCollateralOnClosure", ext.getReleaseCollateralOnClosure());
|
||||||
map.put("topUpAllowed", ext.getTopUpAllowed());
|
map.put("topUpAllowed", ext.getTopUpAllowed());
|
||||||
map.put("balanceTransferOutAllowed", ext.getBalanceTransferOutAllowed());
|
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;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+17
@@ -392,6 +392,9 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
|
||||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
|
||||||
|
|
||||||
|
// An FD is funded once, with exactly its principal (see FixedDepositFundingValidator).
|
||||||
|
new FixedDepositFundingValidator(this.jdbcTemplate).validateFirstFunding(savingsId, transactionAmount);
|
||||||
|
|
||||||
final Map<String, Object> changes = new LinkedHashMap<>();
|
final Map<String, Object> changes = new LinkedHashMap<>();
|
||||||
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
|
||||||
this.denominationBreakdownWriter.validatePreviousDayClosed();
|
this.denominationBreakdownWriter.validatePreviousDayClosed();
|
||||||
@@ -422,6 +425,20 @@ public class DepositAccountWritePlatformServiceJpaRepositoryImpl implements Depo
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public void recalculateFixedDepositMaturity(final Long savingsId) {
|
||||||
|
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
|
||||||
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||||
|
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||||
|
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||||
|
final boolean isPreMatureClosure = false;
|
||||||
|
account.updateMaturityDateAndAmount(MathContext.DECIMAL64, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||||
|
financialYearBeginningMonth);
|
||||||
|
this.savingAccountRepositoryWrapper.saveAndFlush(account);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) {
|
public CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command) {
|
||||||
|
|||||||
+4
-21
@@ -48,7 +48,6 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.deposit.FixedDepositAccountCreateBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.deposit.RecurringDepositAccountCreateBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.deposit.RecurringDepositAccountCreateBusinessEvent;
|
||||||
@@ -184,16 +183,8 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
|||||||
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, submittedBy,
|
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(command, submittedBy,
|
||||||
DepositAccountType.FIXED_DEPOSIT);
|
DepositAccountType.FIXED_DEPOSIT);
|
||||||
|
|
||||||
if (account.getClient() != null && account.productId() != null) {
|
// A client may hold any number of Fixed Deposits on the same product, even while an earlier one is
|
||||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
// still active, so there is deliberately no one-active-account-per-product check here.
|
||||||
account.getClient().getId(), account.productId());
|
|
||||||
if (existingCount > 0) {
|
|
||||||
throw new PlatformDataIntegrityException(
|
|
||||||
"error.msg.savings.account.duplicate.product.for.client",
|
|
||||||
"Client already has an active Fixed Deposit account with the same product.",
|
|
||||||
"productId", account.productId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final MathContext mc = MathContext.DECIMAL64;
|
final MathContext mc = MathContext.DECIMAL64;
|
||||||
final boolean isPreMatureClosure = false;
|
final boolean isPreMatureClosure = false;
|
||||||
@@ -269,16 +260,8 @@ public class DepositApplicationProcessWritePlatformServiceJpaRepositoryImpl impl
|
|||||||
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(command,
|
final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(command,
|
||||||
submittedBy, DepositAccountType.RECURRING_DEPOSIT);
|
submittedBy, DepositAccountType.RECURRING_DEPOSIT);
|
||||||
|
|
||||||
if (account.getClient() != null && account.productId() != null) {
|
// A client may hold any number of Recurring Deposits on the same product, even while an earlier one
|
||||||
long existingCount = this.savingAccountRepository.countActiveAccountsByClientAndProduct(
|
// is still active, so there is deliberately no one-active-account-per-product check here.
|
||||||
account.getClient().getId(), account.productId());
|
|
||||||
if (existingCount > 0) {
|
|
||||||
throw new PlatformDataIntegrityException(
|
|
||||||
"error.msg.savings.account.duplicate.product.for.client",
|
|
||||||
"Client already has an active Recurring Deposit account with the same product.",
|
|
||||||
"productId", account.productId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
|
this.savingsAccountAssembler.applyCbsFields(command, command.parsedJson(), account);
|
||||||
|
|
||||||
|
|||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* 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.savings.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rules a fixed deposit is funded under, shared by every path money can reach an FD by: the FD deposit
|
||||||
|
* command itself (cash at the counter, approved cashier deposit requests) and the clerk's Bank Transfer,
|
||||||
|
* Fund Transfer and Disbursement requests, which credit the account directly.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An FD opens with no money in it and is funded exactly once, with exactly its principal. A second credit
|
||||||
|
* would silently change the principal the maturity was agreed on; anything more goes into a new FD (several
|
||||||
|
* FDs on the same product are allowed). Money never leaves an FD through these paths either - only through
|
||||||
|
* its maturity or premature closure.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When the money comes from the customer's own Savings or Current account, that account must be able to pay
|
||||||
|
* it: active, not debit-frozen, out of any lock-in, belonging to the FD's client (or held jointly with them),
|
||||||
|
* and holding enough available balance once holds, liens, clearing holds and its minimum balance are set
|
||||||
|
* aside. The KYC debit restriction on that account is checked by the callers through KycRestrictionGuard.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class FixedDepositFundingValidator {
|
||||||
|
|
||||||
|
/** m_savings_account.deposit_type_enum (Fineract DepositAccountType). */
|
||||||
|
public static final int DEPOSIT_TYPE_SAVINGS = 100;
|
||||||
|
public static final int DEPOSIT_TYPE_FIXED = 200;
|
||||||
|
|
||||||
|
private static final int STATUS_ACTIVE = 300;
|
||||||
|
private static final int TXN_TYPE_DEPOSIT = 1;
|
||||||
|
/** SavingsAccountSubStatusEnum.BLOCK (no debits or credits) and BLOCK_DEBIT. */
|
||||||
|
private static final int SUB_STATUS_BLOCK = 400;
|
||||||
|
private static final int SUB_STATUS_BLOCK_DEBIT = 600;
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
/** Whether this m_savings_account id is a fixed deposit (a CURRENT account id must not be passed here). */
|
||||||
|
public boolean isFixedDeposit(final Long savingsAccountId) {
|
||||||
|
if (savingsAccountId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_savings_account WHERE id = ? AND deposit_type_enum = ?",
|
||||||
|
Integer.class, savingsAccountId, DEPOSIT_TYPE_FIXED);
|
||||||
|
return count != null && count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FD's first and only funding: the account must be an active, not yet matured fixed deposit with no
|
||||||
|
* money in it, and the amount must be exactly its principal.
|
||||||
|
*
|
||||||
|
* @return the FD's client id, for the source-account checks
|
||||||
|
*/
|
||||||
|
public Long validateFirstFunding(final Long fdAccountId, final BigDecimal amount) {
|
||||||
|
final List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
|
||||||
|
SELECT s.account_no, s.status_enum, s.deposit_type_enum, s.client_id, s.account_balance_derived,
|
||||||
|
t.deposit_amount, t.maturity_date
|
||||||
|
FROM m_savings_account s
|
||||||
|
LEFT JOIN m_deposit_account_term_and_preclosure t ON t.savings_account_id = s.id
|
||||||
|
WHERE s.id = ?
|
||||||
|
""", fdAccountId);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw fail("error.msg.fd.funding.account.not.found", "Fixed deposit account " + fdAccountId + " was not found.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> fd = rows.get(0);
|
||||||
|
final String accountNo = (String) fd.get("account_no");
|
||||||
|
|
||||||
|
if (intOf(fd.get("deposit_type_enum")) != DEPOSIT_TYPE_FIXED) {
|
||||||
|
throw fail("error.msg.fd.funding.not.a.fixed.deposit", "Account " + accountNo + " is not a fixed deposit.");
|
||||||
|
}
|
||||||
|
if (intOf(fd.get("status_enum")) != STATUS_ACTIVE) {
|
||||||
|
throw fail("error.msg.fd.funding.not.active",
|
||||||
|
"Fixed deposit " + accountNo + " is not active. Only an active fixed deposit can be funded.");
|
||||||
|
}
|
||||||
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
|
final LocalDate maturityDate = fd.get("maturity_date") instanceof java.sql.Date d ? d.toLocalDate() : null;
|
||||||
|
if (maturityDate != null && !businessDate.isBefore(maturityDate)) {
|
||||||
|
throw fail("error.msg.fd.funding.matured",
|
||||||
|
"Fixed deposit " + accountNo + " has reached its maturity date (" + maturityDate + ") and can no longer be funded.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Integer deposits = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_transaction WHERE savings_account_id = ? AND transaction_type_enum = ?"
|
||||||
|
+ " AND is_reversed = false",
|
||||||
|
Integer.class, fdAccountId, TXN_TYPE_DEPOSIT);
|
||||||
|
if ((deposits != null && deposits > 0) || amountOf(fd.get("account_balance_derived")).signum() > 0) {
|
||||||
|
throw fail("error.msg.fd.funding.already.funded", "Fixed deposit " + accountNo
|
||||||
|
+ " is already funded. A fixed deposit is funded only once - open a new fixed deposit for more money.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final BigDecimal principal = fd.get("deposit_amount") == null ? null : amountOf(fd.get("deposit_amount"));
|
||||||
|
if (principal == null || principal.signum() <= 0) {
|
||||||
|
throw fail("error.msg.fd.funding.principal.missing", "Fixed deposit " + accountNo + " has no deposit amount (principal) set.");
|
||||||
|
}
|
||||||
|
if (amount == null || amount.compareTo(principal) != 0) {
|
||||||
|
throw fail("error.msg.fd.funding.amount.mismatch", "Amount " + (amount == null ? "0" : amount.toPlainString())
|
||||||
|
+ " for fixed deposit " + accountNo + " must be exactly its principal " + principal.toPlainString() + " - not less, not more.");
|
||||||
|
}
|
||||||
|
return fd.get("client_id") == null ? null : ((Number) fd.get("client_id")).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Savings or Current account an FD is being funded from.
|
||||||
|
*
|
||||||
|
* @param fdAccountId
|
||||||
|
* the FD being funded
|
||||||
|
* @param fdClientId
|
||||||
|
* its client, from {@link #validateFirstFunding}
|
||||||
|
* @param sourceAccountId
|
||||||
|
* m_savings_account id, or m_current_account id when {@code sourceIsCurrent}
|
||||||
|
*/
|
||||||
|
public void validateFundingSource(final Long fdAccountId, final Long fdClientId, final Long sourceAccountId,
|
||||||
|
final boolean sourceIsCurrent, final BigDecimal amount) {
|
||||||
|
final List<Map<String, Object>> rows = sourceIsCurrent ? jdbcTemplate.queryForList("""
|
||||||
|
SELECT account_no, status_enum, sub_status_enum, client_id, account_balance_derived, on_hold_funds_derived,
|
||||||
|
total_savings_amount_on_hold, min_required_balance, enforce_min_required_balance,
|
||||||
|
allow_overdraft, overdraft_limit, lockedin_until_date_derived
|
||||||
|
FROM m_current_account WHERE id = ?
|
||||||
|
""", sourceAccountId) : jdbcTemplate.queryForList("""
|
||||||
|
SELECT account_no, status_enum, sub_status_enum, client_id, deposit_type_enum, account_balance_derived,
|
||||||
|
on_hold_funds_derived, total_savings_amount_on_hold, total_savings_amount_on_hold_clearing,
|
||||||
|
min_required_balance, enforce_min_required_balance, lockedin_until_date_derived
|
||||||
|
FROM m_savings_account WHERE id = ?
|
||||||
|
""", sourceAccountId);
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
throw fail("error.msg.fd.funding.source.not.found", "Source account " + sourceAccountId + " was not found.");
|
||||||
|
}
|
||||||
|
final Map<String, Object> src = rows.get(0);
|
||||||
|
final String accountNo = (String) src.get("account_no");
|
||||||
|
|
||||||
|
if (!sourceIsCurrent && intOf(src.get("deposit_type_enum")) != DEPOSIT_TYPE_SAVINGS) {
|
||||||
|
throw fail("error.msg.fd.funding.source.type",
|
||||||
|
"Account " + accountNo + " cannot fund a fixed deposit - only a Savings or Current account can.");
|
||||||
|
}
|
||||||
|
if (intOf(src.get("status_enum")) != STATUS_ACTIVE) {
|
||||||
|
throw fail("error.msg.fd.funding.source.not.active", "Source account " + accountNo + " is not active.");
|
||||||
|
}
|
||||||
|
final int subStatus = intOf(src.get("sub_status_enum"));
|
||||||
|
if (subStatus == SUB_STATUS_BLOCK || subStatus == SUB_STATUS_BLOCK_DEBIT) {
|
||||||
|
throw fail("error.msg.fd.funding.source.debit.frozen", "Source account " + accountNo + " is blocked for debits (debit freeze).");
|
||||||
|
}
|
||||||
|
final LocalDate lockedUntil = src.get("lockedin_until_date_derived") instanceof java.sql.Date d ? d.toLocalDate() : null;
|
||||||
|
if (lockedUntil != null && DateUtils.getBusinessLocalDate().isBefore(lockedUntil)) {
|
||||||
|
throw fail("error.msg.fd.funding.source.locked.in", "Source account " + accountNo + " is locked-in until " + lockedUntil + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long sourceClientId = src.get("client_id") == null ? null : ((Number) src.get("client_id")).longValue();
|
||||||
|
if (!sameClientOrJointHolder(fdAccountId, fdClientId, sourceAccountId, sourceIsCurrent, sourceClientId)) {
|
||||||
|
throw fail("error.msg.fd.funding.source.other.client", "Source account " + accountNo
|
||||||
|
+ " does not belong to the fixed deposit's customer. Fund an FD only from the customer's own account"
|
||||||
|
+ " or one they hold jointly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal available = amountOf(src.get("account_balance_derived")).subtract(amountOf(src.get("on_hold_funds_derived")))
|
||||||
|
.subtract(amountOf(src.get("total_savings_amount_on_hold")));
|
||||||
|
if (!sourceIsCurrent) {
|
||||||
|
available = available.subtract(amountOf(src.get("total_savings_amount_on_hold_clearing")));
|
||||||
|
}
|
||||||
|
if (Boolean.TRUE.equals(src.get("enforce_min_required_balance"))) {
|
||||||
|
available = available.subtract(amountOf(src.get("min_required_balance")));
|
||||||
|
}
|
||||||
|
if (sourceIsCurrent && Boolean.TRUE.equals(src.get("allow_overdraft"))) {
|
||||||
|
available = available.add(amountOf(src.get("overdraft_limit")));
|
||||||
|
}
|
||||||
|
if (amount == null || available.compareTo(amount) < 0) {
|
||||||
|
throw fail("error.msg.fd.funding.source.insufficient", "Source account " + accountNo + " has an available balance of "
|
||||||
|
+ available.max(BigDecimal.ZERO).toPlainString() + " (after holds, lien and minimum balance), less than "
|
||||||
|
+ (amount == null ? "0" : amount.toPlainString()) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameClientOrJointHolder(final Long fdAccountId, final Long fdClientId, final Long sourceAccountId,
|
||||||
|
final boolean sourceIsCurrent, final Long sourceClientId) {
|
||||||
|
if (fdClientId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Objects.equals(fdClientId, sourceClientId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// The FD's customer is a joint holder on the source account ...
|
||||||
|
final Integer onSource = sourceIsCurrent
|
||||||
|
? jdbcTemplate.queryForObject("SELECT COUNT(*) FROM m_current_account_joint_holder WHERE current_account_id = ? AND client_id = ?",
|
||||||
|
Integer.class, sourceAccountId, fdClientId)
|
||||||
|
: jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_joint_holders WHERE savings_account_id = ? AND client_id = ?", Integer.class,
|
||||||
|
sourceAccountId, fdClientId);
|
||||||
|
if (onSource != null && onSource > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// ... or the source account's customer is a joint holder on the FD.
|
||||||
|
if (sourceClientId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Integer onFd = jdbcTemplate.queryForObject(
|
||||||
|
"SELECT COUNT(*) FROM m_savings_account_joint_holders WHERE savings_account_id = ? AND client_id = ?", Integer.class,
|
||||||
|
fdAccountId, sourceClientId);
|
||||||
|
return onFd != null && onFd > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int intOf(final Object value) {
|
||||||
|
return value instanceof Number n ? n.intValue() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal amountOf(final Object value) {
|
||||||
|
return value instanceof BigDecimal b ? b : value instanceof Number n ? new BigDecimal(n.toString()) : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GeneralPlatformDomainRuleException fail(final String code, final String message) {
|
||||||
|
return new GeneralPlatformDomainRuleException(code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -623,17 +623,6 @@
|
|||||||
<include file="parts/0595_seed_loan_rate_register_gl_accounts.xml" relativeToChangelogFile="true"/>
|
<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/0596_enable_delinquency_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0597_add_loan_product_loans_ext_fields.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
|
<!-- 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. -->
|
each one builds on the core the block above sets up and none of it builds on DDS. -->
|
||||||
|
|||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-167
@@ -1,167 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
<?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>
|
|
||||||
-47
@@ -1,47 +0,0 @@
|
|||||||
<?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>
|
|
||||||
+2
-2
@@ -173,7 +173,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
|
|||||||
when(loanAccountData.getId()).thenReturn(1L);
|
when(loanAccountData.getId()).thenReturn(1L);
|
||||||
when(loanAccountData.getAccountNo()).thenReturn("0001");
|
when(loanAccountData.getAccountNo()).thenReturn("0001");
|
||||||
when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
|
when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
|
||||||
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10, null));
|
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10));
|
||||||
when(loanAccountData.getCurrency()).thenAnswer(
|
when(loanAccountData.getCurrency()).thenAnswer(
|
||||||
a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf()));
|
a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf()));
|
||||||
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
||||||
@@ -238,7 +238,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
|
|||||||
when(loanAccountData.getId()).thenReturn(1L);
|
when(loanAccountData.getId()).thenReturn(1L);
|
||||||
when(loanAccountData.getAccountNo()).thenReturn("0001");
|
when(loanAccountData.getAccountNo()).thenReturn("0001");
|
||||||
when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
|
when(loanAccountData.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
|
||||||
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10, null));
|
when(loanAccountData.getDelinquencyRange()).thenReturn(new DelinquencyRangeData(1L, "classification", 1, 10));
|
||||||
when(loanAccountData.getCurrency()).thenAnswer(
|
when(loanAccountData.getCurrency()).thenAnswer(
|
||||||
a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf()));
|
a -> new CurrencyData(loanCurrency.getCode(), loanCurrency.getDigitsAfterDecimal(), loanCurrency.getInMultiplesOf()));
|
||||||
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
|
||||||
|
|||||||
+14
-14
@@ -174,9 +174,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
||||||
@@ -217,9 +217,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
||||||
@@ -281,9 +281,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
|
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
|
|
||||||
@@ -346,9 +346,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
||||||
@@ -425,9 +425,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();
|
||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
||||||
@@ -510,9 +510,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
// given
|
// given
|
||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
||||||
@@ -597,9 +597,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
// given
|
// given
|
||||||
Loan loanForProcessing = Mockito.mock(Loan.class);
|
Loan loanForProcessing = Mockito.mock(Loan.class);
|
||||||
when(loanForProcessing.getId()).thenReturn(1L);
|
when(loanForProcessing.getId()).thenReturn(1L);
|
||||||
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2, null);
|
DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2);
|
||||||
range1.setId(1L);
|
range1.setId(1L);
|
||||||
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30, null);
|
DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30);
|
||||||
range2.setId(2L);
|
range2.setId(2L);
|
||||||
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
List<DelinquencyRange> listDelinquencyRanges = Arrays.asList(range1, range2);
|
||||||
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket");
|
||||||
|
|||||||
+7
@@ -39,6 +39,13 @@ public interface DepositAccountWritePlatformService {
|
|||||||
|
|
||||||
CommandProcessingResult depositToFDAccount(Long savingsId, JsonCommand command);
|
CommandProcessingResult depositToFDAccount(Long savingsId, JsonCommand command);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recomputes a fixed deposit's maturity date and amount from its real transaction history. Used after an FD
|
||||||
|
* is funded by a direct book credit (clerk Bank Transfer / Fund Transfer / Disbursement), which does not go
|
||||||
|
* through {@link #depositToFDAccount}.
|
||||||
|
*/
|
||||||
|
void recalculateFixedDepositMaturity(Long savingsId);
|
||||||
|
|
||||||
CommandProcessingResult depositToRDAccount(Long savingsId, JsonCommand command);
|
CommandProcessingResult depositToRDAccount(Long savingsId, JsonCommand command);
|
||||||
|
|
||||||
CommandProcessingResult withdrawal(Long savingsId, JsonCommand command, DepositAccountType depositAccountType);
|
CommandProcessingResult withdrawal(Long savingsId, JsonCommand command, DepositAccountType depositAccountType);
|
||||||
|
|||||||
Reference in New Issue
Block a user