feat(loan-origination): priority sector classification at KYC, deposit-loan sanction without checklists, lien release
- KYC completion now requires the loan purpose, the priority sector decision (with its category) and the weaker section decision (with its type), checked on the server. - Deposit loans: no CERSAI / property / previous-loan-declaration requirement at sanction, even for an application created before its product was known to be a deposit product. - Priority sector categories and weaker section types are seeded as Codes (PrioritySectorCategory, WeakerSectionType) so they can be maintained in Admin. - DdsLienReleaseService lifts the DDS lien when the loan it secures ends. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package org.apache.fineract.portfolio.loanoriginationpipeline.service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
|
||||||
|
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
|
||||||
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Daily Deposit Scheme account pledged for a loan is held with a lien whose reason names the loan
|
||||||
|
* ({@link #reasonFor}). The DDS module and the loan module are not connected, so nothing else would ever lift
|
||||||
|
* that hold: this lifts it when the loan stops being live (repaid, closed, written off, withdrawn).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* FD and RD liens need nothing like this: they are Fineract guarantor funding, which Fineract releases itself as the
|
||||||
|
* loan is repaid.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class DdsLienReleaseService {
|
||||||
|
|
||||||
|
private static final String LIEN_PREFIX = "Held for loan ";
|
||||||
|
|
||||||
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
/** The text written on the DDS account's lien, and what is matched when the loan ends. */
|
||||||
|
public static String reasonFor(final String loanAccountNo, final String applicationNo) {
|
||||||
|
return LIEN_PREFIX + loanAccountNo + " (" + applicationNo + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void addListeners() {
|
||||||
|
this.businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class,
|
||||||
|
new ReleaseOnLoanEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ReleaseOnLoanEnd implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBusinessEvent(final LoanStatusChangedBusinessEvent event) {
|
||||||
|
final Loan loan = event.get();
|
||||||
|
if (loan == null || loan.getStatus() == null || loan.getStatus().isActiveOrAwaitingApprovalOrDisbursal()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int released = jdbcTemplate.update(
|
||||||
|
"UPDATE m_dds_party SET lien = FALSE, lien_reason = NULL WHERE lien = TRUE AND lien_reason LIKE ?",
|
||||||
|
LIEN_PREFIX + loan.getAccountNumber() + " (%");
|
||||||
|
if (released > 0) {
|
||||||
|
log.info("Released the lien on {} DDS account(s) held for loan {}", released, loan.getAccountNumber());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-2
@@ -185,12 +185,43 @@ public class LoanOriginationPipelineService {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public String save(final String applicationNo, final String requestBodyAsJson) {
|
public String save(final String applicationNo, final String requestBodyAsJson) {
|
||||||
|
final JsonObject stored = loadApplication(applicationNo);
|
||||||
// Once a stage is sent for approval nothing on the application may change until it is decided or withdrawn,
|
// Once a stage is sent for approval nothing on the application may change until it is decided or withdrawn,
|
||||||
// so a full-object save is ignored and the stored application is returned as it is.
|
// so a full-object save is ignored and the stored application is returned as it is.
|
||||||
if (hasPendingApprovalRequest(loadApplication(applicationNo))) {
|
if (hasPendingApprovalRequest(stored)) {
|
||||||
return findOneAsJson(applicationNo);
|
return findOneAsJson(applicationNo);
|
||||||
}
|
}
|
||||||
return storeApplication(applicationNo, parseObject(requestBodyAsJson));
|
// A disbursed or rejected application is final. A screen that was opened before that happened still holds the
|
||||||
|
// older copy, and saving it back would undo the disbursement (and lose the link to the loan account).
|
||||||
|
final String storedStatus = stringOrNull(stored, "overallStatus");
|
||||||
|
if ("DISBURSED".equals(storedStatus) || "REJECTED".equals(storedStatus)) {
|
||||||
|
return findOneAsJson(applicationNo);
|
||||||
|
}
|
||||||
|
final JsonObject incoming = parseObject(requestBodyAsJson);
|
||||||
|
// A copy that is further back than what is stored is a stale screen: another user or a stage endpoint has moved the
|
||||||
|
// application on since it was opened. Its save is ignored and the stored application is returned, so the screen
|
||||||
|
// picks up the real state instead of overwriting it.
|
||||||
|
if (furthestStageOf(incoming) < furthestStageOf(stored)) {
|
||||||
|
return findOneAsJson(applicationNo);
|
||||||
|
}
|
||||||
|
// What only the server decides (approval decisions and requests, the loan account it was disbursed to) is never
|
||||||
|
// taken from the screen's copy.
|
||||||
|
for (final String key : SERVER_OWNED_FIELDS) {
|
||||||
|
if (stored.has(key)) {
|
||||||
|
incoming.add(key, stored.get(key).deepCopy());
|
||||||
|
} else {
|
||||||
|
incoming.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return storeApplication(applicationNo, incoming);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final java.util.List<String> SERVER_OWNED_FIELDS = java.util.List.of("creditCommitteeApproval", "riskApproval",
|
||||||
|
"legalApproval", "postCheckApproval", "disbursementApproval", "approvalRequests", "fineractLoanId", "fineractLoanAccountNo");
|
||||||
|
|
||||||
|
private int furthestStageOf(final JsonObject app) {
|
||||||
|
final JsonElement value = app.get("furthestStageIndex");
|
||||||
|
return value != null && value.isJsonPrimitive() && value.getAsJsonPrimitive().isNumber() ? value.getAsInt() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasPendingApprovalRequest(final JsonObject app) {
|
private boolean hasPendingApprovalRequest(final JsonObject app) {
|
||||||
|
|||||||
+213
-22
@@ -40,6 +40,7 @@ import java.util.Set;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ public class LoanOriginationStageService {
|
|||||||
private static final Set<String> DEPOSIT_PRODUCT_CODES = Set.of("FD", "RD", "DS", "LI");
|
private static final Set<String> DEPOSIT_PRODUCT_CODES = Set.of("FD", "RD", "DS", "LI");
|
||||||
|
|
||||||
private final LoanOriginationPipelineService pipelineService;
|
private final LoanOriginationPipelineService pipelineService;
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
/** A loan above the product's limit on the deposit (85%) is allowed only as a recorded officer's discretion with a reason. */
|
/** A loan above the product's limit on the deposit (85%) is allowed only as a recorded officer's discretion with a reason. */
|
||||||
private void requireWithinLimitOrException(final JsonObject app, final JsonObject selection) {
|
private void requireWithinLimitOrException(final JsonObject app, final JsonObject selection) {
|
||||||
@@ -95,9 +97,34 @@ public class LoanOriginationStageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Product Creation sub types of the loans against a deposit or policy. */
|
||||||
|
private static final Set<String> DEPOSIT_SUB_TYPES = Set.of("fd", "rd", "dds", "lic");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A loan against a deposit is told by the product's own sub type (chosen in Product Creation), so a product made
|
||||||
|
* later under any short name (a second FD product, say) follows the deposit flow. The fixed short names stay as
|
||||||
|
* the answer for the original products.
|
||||||
|
*/
|
||||||
private boolean isDepositLoan(final JsonObject app) {
|
private boolean isDepositLoan(final JsonObject app) {
|
||||||
final String code = str(app, "productCode");
|
final String code = str(app, "productCode");
|
||||||
return code != null && DEPOSIT_PRODUCT_CODES.contains(code);
|
if (code == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return DEPOSIT_PRODUCT_CODES.contains(code) || DEPOSIT_SUB_TYPES.contains(productSubType(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLicLoan(final JsonObject app) {
|
||||||
|
final String code = str(app, "productCode");
|
||||||
|
return code != null && ("LI".equals(code) || "lic".equals(productSubType(code)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The sub type saved with the loan product of this short name; empty when the product has none. */
|
||||||
|
private String productSubType(final String shortName) {
|
||||||
|
final List<String> found = jdbcTemplate.queryForList(
|
||||||
|
"select e.product_type_sub_group from m_product_loan p join m_product_loan_advance_ext e on e.loan_product_id = p.id"
|
||||||
|
+ " where p.short_name = ?",
|
||||||
|
String.class, shortName);
|
||||||
|
return found.isEmpty() || found.get(0) == null ? "" : found.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── KYC & Documents ──────────────────────────────────────────────────────
|
// ── KYC & Documents ──────────────────────────────────────────────────────
|
||||||
@@ -110,6 +137,7 @@ public class LoanOriginationStageService {
|
|||||||
|
|
||||||
require(bool(app, "aadhaarVerified"), "Aadhaar has not been verified.");
|
require(bool(app, "aadhaarVerified"), "Aadhaar has not been verified.");
|
||||||
require(bool(app, "panVerified"), "PAN has not been verified.");
|
require(bool(app, "panVerified"), "PAN has not been verified.");
|
||||||
|
requirePurposeAndPriorityClassification(app);
|
||||||
final boolean depositLoan = isDepositLoan(app);
|
final boolean depositLoan = isDepositLoan(app);
|
||||||
if (!depositLoan) {
|
if (!depositLoan) {
|
||||||
// For a loan against a deposit these are optional: the deposit is the security.
|
// For a loan against a deposit these are optional: the deposit is the security.
|
||||||
@@ -123,6 +151,10 @@ public class LoanOriginationStageService {
|
|||||||
require(bool(doc, "submitted"), "Required document not submitted: " + strOr(doc, "label", strOr(doc, "key", "document")));
|
require(bool(doc, "submitted"), "Required document not submitted: " + strOr(doc, "label", strOr(doc, "key", "document")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (app.has("guarantorsRequiredCount")) {
|
||||||
|
requireGuarantorsNamed(app);
|
||||||
|
}
|
||||||
|
|
||||||
advance(app, actor(input), "KYC Complete", strOr(input, "remarks", "KYC and document collection complete."));
|
advance(app, actor(input), "KYC Complete", strOr(input, "remarks", "KYC and document collection complete."));
|
||||||
if (!depositLoan) {
|
if (!depositLoan) {
|
||||||
// Deposit selection only exists for a loan against a deposit; every other product passes straight through.
|
// Deposit selection only exists for a loan against a deposit; every other product passes straight through.
|
||||||
@@ -131,6 +163,25 @@ public class LoanOriginationStageService {
|
|||||||
return pipelineService.storeApplication(applicationNo, app);
|
return pipelineService.storeApplication(applicationNo, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every loan states what it is for and how it is classified under the RBI priority sector lending directions before
|
||||||
|
* KYC is complete: the purpose, the priority sector decision (with its category when it is priority sector) and
|
||||||
|
* whether the borrower is a weaker section (with its type when it is).
|
||||||
|
*/
|
||||||
|
private void requirePurposeAndPriorityClassification(final JsonObject app) {
|
||||||
|
require(!strOr(app, "loanPurpose", "").isBlank(), "The purpose of the loan has not been entered.");
|
||||||
|
final String priority = strOr(app, "pslClassified", "");
|
||||||
|
require("yes".equals(priority) || "no".equals(priority), "The priority sector decision has not been made.");
|
||||||
|
if ("yes".equals(priority)) {
|
||||||
|
require(!strOr(app, "prioritySectorCategory", "").isBlank(), "The priority sector category has not been chosen.");
|
||||||
|
}
|
||||||
|
final String weaker = strOr(app, "weakerSection", "");
|
||||||
|
require("yes".equals(weaker) || "no".equals(weaker), "Whether the borrower is a weaker section has not been decided.");
|
||||||
|
if ("yes".equals(weaker)) {
|
||||||
|
require(!strOr(app, "weakerSectionType", "").isBlank(), "The weaker section type has not been chosen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Deposit Selection & Eligibility (loan against FD / RD / DDS / LIC) ───
|
// ── Deposit Selection & Eligibility (loan against FD / RD / DDS / LIC) ───
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -142,7 +193,15 @@ public class LoanOriginationStageService {
|
|||||||
|
|
||||||
final JsonObject selection = obj(app, "depositSelection");
|
final JsonObject selection = obj(app, "depositSelection");
|
||||||
require(selection != null && bool(selection, "saved"), "No deposit has been selected and saved for this application.");
|
require(selection != null && bool(selection, "saved"), "No deposit has been selected and saved for this application.");
|
||||||
require(!arr(selection, "accountIds").isEmpty(), "Select at least one deposit.");
|
if (isLicLoan(app)) {
|
||||||
|
// A LIC policy is not a bank account: it is recorded on the application from the policy documents.
|
||||||
|
final JsonObject policy = obj(selection, "policy");
|
||||||
|
require(policy != null && !strOr(policy, "policyNo", "").isBlank(), "The LIC policy number has not been recorded.");
|
||||||
|
require(intOr(policy, "surrenderValue", 0) > 0, "The surrender value of the policy has not been recorded.");
|
||||||
|
require(!strOr(policy, "maturityDate", "").isBlank(), "The maturity date of the policy has not been recorded.");
|
||||||
|
} else {
|
||||||
|
require(!arr(selection, "accountIds").isEmpty(), "Select at least one deposit.");
|
||||||
|
}
|
||||||
require(intOr(selection, "eligibleAmount", 0) >= intOr(app, "requestedAmount", 0),
|
require(intOr(selection, "eligibleAmount", 0) >= intOr(app, "requestedAmount", 0),
|
||||||
"The requested amount is more than the eligible amount on the selected deposits.");
|
"The requested amount is more than the eligible amount on the selected deposits.");
|
||||||
require(intOr(app, "tenureMonths", 0) <= intOr(selection, "maxTenure", 0),
|
require(intOr(app, "tenureMonths", 0) <= intOr(selection, "maxTenure", 0),
|
||||||
@@ -309,6 +368,8 @@ public class LoanOriginationStageService {
|
|||||||
case "riskApproval" -> "RISK_APPROVAL";
|
case "riskApproval" -> "RISK_APPROVAL";
|
||||||
case "legalApproval" -> "LEGAL_APPROVAL";
|
case "legalApproval" -> "LEGAL_APPROVAL";
|
||||||
case "postCheckApproval" -> "POST_CHECK";
|
case "postCheckApproval" -> "POST_CHECK";
|
||||||
|
// Sending the disbursement for approval: it is asked at the Disbursement stage and does not move the application on.
|
||||||
|
case "disbursementApproval" -> "DISBURSED";
|
||||||
default -> throw badRequest("Unknown approval stage: " + field);
|
default -> throw badRequest("Unknown approval stage: " + field);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -319,10 +380,22 @@ public class LoanOriginationStageService {
|
|||||||
case "riskApproval" -> requireRiskChecklistReady(app);
|
case "riskApproval" -> requireRiskChecklistReady(app);
|
||||||
case "legalApproval" -> requireLegalChecklistDone(app);
|
case "legalApproval" -> requireLegalChecklistDone(app);
|
||||||
case "postCheckApproval" -> requirePostCheckSignatures(app);
|
case "postCheckApproval" -> requirePostCheckSignatures(app);
|
||||||
|
case "disbursementApproval" -> requireSanctionApprovalsStanding(app);
|
||||||
default -> throw badRequest("Unknown approval stage: " + field);
|
default -> throw badRequest("Unknown approval stage: " + field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The four sanctioning approvals must all stand before a disbursement can be sent for approval. */
|
||||||
|
private void requireSanctionApprovalsStanding(final JsonObject app) {
|
||||||
|
final List<String> fields = isDepositLoan(app) ? List.of("creditCommitteeApproval", "postCheckApproval")
|
||||||
|
: List.of("creditCommitteeApproval", "riskApproval", "legalApproval", "postCheckApproval");
|
||||||
|
for (final String field : fields) {
|
||||||
|
final JsonObject approval = obj(app, field);
|
||||||
|
final String decision = approval == null ? null : str(approval, "decision");
|
||||||
|
require("APPROVED".equals(decision) || "EXCEPTION_APPROVED".equals(decision), "Cannot send for disbursement approval: " + field + " is not approved.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private JsonObject pendingRequest(final JsonObject app, final String field) {
|
private JsonObject pendingRequest(final JsonObject app, final String field) {
|
||||||
final JsonObject requests = obj(app, "approvalRequests");
|
final JsonObject requests = obj(app, "approvalRequests");
|
||||||
final JsonObject request = requests == null ? null : obj(requests, field);
|
final JsonObject request = requests == null ? null : obj(requests, field);
|
||||||
@@ -358,6 +431,13 @@ public class LoanOriginationStageService {
|
|||||||
if (input.has("checklist") && input.get("checklist").isJsonArray()) {
|
if (input.has("checklist") && input.get("checklist").isJsonArray()) {
|
||||||
request.add("checklist", input.getAsJsonArray("checklist"));
|
request.add("checklist", input.getAsJsonArray("checklist"));
|
||||||
}
|
}
|
||||||
|
final JsonObject oldRequests = obj(app, "approvalRequests");
|
||||||
|
final JsonObject oldRequest = oldRequests == null ? null : obj(oldRequests, field);
|
||||||
|
if (oldRequest != null && "REVOKED".equals(str(oldRequest, "status"))) {
|
||||||
|
request.addProperty("previousRevokeRemarks", strOr(oldRequest, "revokeRemarks", str(oldRequest, "remarks")));
|
||||||
|
request.addProperty("previousRevokedBy", str(oldRequest, "decidedBy"));
|
||||||
|
request.addProperty("previousRevokedOn", str(oldRequest, "decidedOn"));
|
||||||
|
}
|
||||||
objOrCreate(app, "approvalRequests").add(field, request);
|
objOrCreate(app, "approvalRequests").add(field, request);
|
||||||
|
|
||||||
appendHistory(app, expectedStage, user, "Sent for Approval", remarks);
|
appendHistory(app, expectedStage, user, "Sent for Approval", remarks);
|
||||||
@@ -402,14 +482,14 @@ public class LoanOriginationStageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checker step: a different user approves or rejects what was sent. */
|
/** Checker step: a different user approves, rejects or revokes (returns for rectification) what was sent. */
|
||||||
@Transactional
|
@Transactional
|
||||||
public String decideApproval(final String applicationNo, final String field, final String body, final String user) {
|
public String decideApproval(final String applicationNo, final String field, final String body, final String user) {
|
||||||
final JsonObject app = pipelineService.loadApplication(applicationNo);
|
final JsonObject app = pipelineService.loadApplication(applicationNo);
|
||||||
final JsonObject input = parseObject(body);
|
final JsonObject input = parseObject(body);
|
||||||
|
|
||||||
String decision = strOr(input, "decision", "APPROVED");
|
String decision = strOr(input, "decision", "APPROVED");
|
||||||
require(List.of("APPROVED", "REJECTED", "EXCEPTION_APPROVED").contains(decision), "Unknown decision: " + decision);
|
require(List.of("APPROVED", "REJECTED", "EXCEPTION_APPROVED", "REVOKED").contains(decision), "Unknown decision: " + decision);
|
||||||
|
|
||||||
final String expectedStage = approvalStage(field);
|
final String expectedStage = approvalStage(field);
|
||||||
requireStage(app, expectedStage);
|
requireStage(app, expectedStage);
|
||||||
@@ -417,33 +497,55 @@ public class LoanOriginationStageService {
|
|||||||
final JsonObject request = pendingRequest(app, field);
|
final JsonObject request = pendingRequest(app, field);
|
||||||
require(request != null, "This stage has not been sent for approval yet.");
|
require(request != null, "This stage has not been sent for approval yet.");
|
||||||
require(!user.equalsIgnoreCase(str(request, "submittedBy")),
|
require(!user.equalsIgnoreCase(str(request, "submittedBy")),
|
||||||
"The person who sent this for approval cannot approve or reject it. Ask another user.");
|
"The person who sent this for approval cannot approve, reject or revoke it. Ask another user.");
|
||||||
if (!"REJECTED".equals(decision)) {
|
|
||||||
decision = strOr(request, "requestedDecision", "APPROVED");
|
String remarks = strOr(input, "remarks", "").trim();
|
||||||
|
if ("REVOKED".equals(decision)) {
|
||||||
|
require(!remarks.isBlank(), "Remarks are required when returning an approval request for correction.");
|
||||||
|
} else if ("REJECTED".equals(decision)) {
|
||||||
|
require(!remarks.isBlank(), "Give the reason for rejecting in the remarks box.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!"REJECTED".equals(decision)) {
|
if (!"REJECTED".equals(decision) && !"REVOKED".equals(decision)) {
|
||||||
|
decision = strOr(request, "requestedDecision", "APPROVED");
|
||||||
requireCheckerVerified(request, input);
|
requireCheckerVerified(request, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prerequisites are only enforced for a positive decision: a rejection has to remain possible
|
// Prerequisites are only enforced for a positive decision: a rejection or revocation has to remain possible
|
||||||
// at any point, and blocking it on an incomplete checklist would trap the application.
|
// at any point, and blocking it on an incomplete checklist would trap the application.
|
||||||
final JsonObject resolution = new JsonObject();
|
final JsonObject resolution = new JsonObject();
|
||||||
resolution.addProperty("resolutionNo", strOr(request, "resolutionNo", ""));
|
resolution.addProperty("resolutionNo", strOr(request, "resolutionNo", ""));
|
||||||
resolution.addProperty("resolutionDate", strOr(request, "resolutionDate", ""));
|
resolution.addProperty("resolutionDate", strOr(request, "resolutionDate", ""));
|
||||||
if (!"REJECTED".equals(decision)) {
|
if (!"REJECTED".equals(decision) && !"REVOKED".equals(decision)) {
|
||||||
checkApprovalPrerequisites(app, field, resolution);
|
checkApprovalPrerequisites(app, field, resolution);
|
||||||
}
|
}
|
||||||
|
|
||||||
String remarks = strOr(input, "remarks", "REJECTED".equals(decision) ? "Declined on review." : "Reviewed and approved.");
|
if (remarks.isBlank()) {
|
||||||
if ("creditCommitteeApproval".equals(field) && !"REJECTED".equals(decision)) {
|
remarks = "REJECTED".equals(decision) ? "Declined on review." : "Reviewed and approved.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("creditCommitteeApproval".equals(field) && !"REJECTED".equals(decision) && !"REVOKED".equals(decision)) {
|
||||||
// The resolution number and date are what the sanction letter later quotes, so they are
|
// The resolution number and date are what the sanction letter later quotes, so they are
|
||||||
// recorded on the application and restated in the audit trail.
|
// recorded on the application and restated in the audit trail.
|
||||||
final String resolutionNo = strOr(resolution, "resolutionNo", "");
|
final boolean depositSanction = isDepositLoan(app);
|
||||||
final String resolutionDate = strOr(resolution, "resolutionDate", "");
|
final String resolutionNo = depositSanction ? applicationNo : strOr(resolution, "resolutionNo", "");
|
||||||
|
final String resolutionDate = depositSanction ? today() : strOr(resolution, "resolutionDate", "");
|
||||||
app.addProperty("committeeResolutionNo", resolutionNo);
|
app.addProperty("committeeResolutionNo", resolutionNo);
|
||||||
app.addProperty("committeeResolutionDate", resolutionDate);
|
app.addProperty("committeeResolutionDate", resolutionDate);
|
||||||
remarks = remarks + " (Resolution No. " + resolutionNo + " dated " + resolutionDate + ")";
|
remarks = remarks + (depositSanction ? " (Sanction No. " : " (Resolution No. ") + resolutionNo + " dated " + resolutionDate + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("REVOKED".equals(decision)) {
|
||||||
|
request.addProperty("status", "REVOKED");
|
||||||
|
request.addProperty("decidedBy", user);
|
||||||
|
request.addProperty("decidedOn", today());
|
||||||
|
request.addProperty("revokeRemarks", remarks);
|
||||||
|
request.addProperty("remarks", remarks);
|
||||||
|
if (app.has(field)) {
|
||||||
|
app.remove(field);
|
||||||
|
}
|
||||||
|
appendHistory(app, expectedStage, user, "Returned for Rectification", remarks);
|
||||||
|
return pipelineService.storeApplication(applicationNo, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
final JsonObject approval = objOrCreate(app, field);
|
final JsonObject approval = objOrCreate(app, field);
|
||||||
@@ -460,11 +562,24 @@ public class LoanOriginationStageService {
|
|||||||
request.addProperty("decidedBy", user);
|
request.addProperty("decidedBy", user);
|
||||||
request.addProperty("decidedOn", today());
|
request.addProperty("decidedOn", today());
|
||||||
|
|
||||||
if ("REJECTED".equals(decision)) {
|
if ("disbursementApproval".equals(field)) {
|
||||||
|
// Approving the disbursement does not move the stage (the loan is disbursed next); declining it only sends it back.
|
||||||
|
if ("REJECTED".equals(decision)) {
|
||||||
|
app.remove(field);
|
||||||
|
appendHistory(app, expectedStage, user, "Disbursement Declined", remarks);
|
||||||
|
} else {
|
||||||
|
appendHistory(app, expectedStage, user, "Disbursement Approved", remarks);
|
||||||
|
}
|
||||||
|
} else if ("REJECTED".equals(decision)) {
|
||||||
app.addProperty("overallStatus", "REJECTED");
|
app.addProperty("overallStatus", "REJECTED");
|
||||||
appendHistory(app, str(app, "currentStage"), user, "Rejected", remarks);
|
appendHistory(app, str(app, "currentStage"), user, "Rejected", remarks);
|
||||||
} else {
|
} else {
|
||||||
advance(app, user, "EXCEPTION_APPROVED".equals(decision) ? "Approved on Exception" : "Approved", remarks);
|
advance(app, user, "EXCEPTION_APPROVED".equals(decision) ? "Approved on Exception" : "Approved", remarks);
|
||||||
|
if ("creditCommitteeApproval".equals(field) && isDepositLoan(app)) {
|
||||||
|
// Sanctioned: no risk approval and no legal approval on a loan against a deposit (the legal documents are taken at signing).
|
||||||
|
advance(app, user, "Risk Approval Not Required", "Loan against a deposit - sanctioned by the Sanction step.");
|
||||||
|
advance(app, user, "Legal Approval Not Required", "Loan against a deposit - the legal documents are taken at Customer Signing.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pipelineService.storeApplication(applicationNo, app);
|
return pipelineService.storeApplication(applicationNo, app);
|
||||||
}
|
}
|
||||||
@@ -475,6 +590,10 @@ public class LoanOriginationStageService {
|
|||||||
final JsonObject input = parseObject(body);
|
final JsonObject input = parseObject(body);
|
||||||
requireStage(app, "CUSTOMER_SIGNING");
|
requireStage(app, "CUSTOMER_SIGNING");
|
||||||
require(filled(app, "agreementSigningSlot"), "No agreement signing slot has been recorded.");
|
require(filled(app, "agreementSigningSlot"), "No agreement signing slot has been recorded.");
|
||||||
|
if (isDepositLoan(app)) {
|
||||||
|
// No legal approval stage on a loan against a deposit: its legal documents (stamp duty, DP note, lien letter ...) are taken here.
|
||||||
|
requireLegalChecklistDone(app);
|
||||||
|
}
|
||||||
|
|
||||||
app.addProperty("customerSigningDate", today());
|
app.addProperty("customerSigningDate", today());
|
||||||
advance(app, strOr(input, "actionBy", strOr(app, "applicantName", "Customer")), "Signed",
|
advance(app, strOr(input, "actionBy", strOr(app, "applicantName", "Customer")), "Signed",
|
||||||
@@ -518,7 +637,9 @@ public class LoanOriginationStageService {
|
|||||||
requireStage(app, "DISBURSED");
|
requireStage(app, "DISBURSED");
|
||||||
require(!"DISBURSED".equals(str(app, "overallStatus")), "This application has already been disbursed.");
|
require(!"DISBURSED".equals(str(app, "overallStatus")), "This application has already been disbursed.");
|
||||||
|
|
||||||
for (final String field : List.of("creditCommitteeApproval", "riskApproval", "legalApproval", "postCheckApproval")) {
|
final List<String> mustStand = isDepositLoan(app) ? List.of("creditCommitteeApproval", "postCheckApproval")
|
||||||
|
: List.of("creditCommitteeApproval", "riskApproval", "legalApproval", "postCheckApproval");
|
||||||
|
for (final String field : mustStand) {
|
||||||
final JsonObject approval = obj(app, field);
|
final JsonObject approval = obj(app, field);
|
||||||
final String decision = approval == null ? null : str(approval, "decision");
|
final String decision = approval == null ? null : str(approval, "decision");
|
||||||
require("APPROVED".equals(decision) || "EXCEPTION_APPROVED".equals(decision),
|
require("APPROVED".equals(decision) || "EXCEPTION_APPROVED".equals(decision),
|
||||||
@@ -548,12 +669,38 @@ public class LoanOriginationStageService {
|
|||||||
app.addProperty("fineractLoanId", LoanOriginationJson.lng(input, "fineractLoanId"));
|
app.addProperty("fineractLoanId", LoanOriginationJson.lng(input, "fineractLoanId"));
|
||||||
app.addProperty("fineractLoanAccountNo", str(input, "fineractLoanAccountNo"));
|
app.addProperty("fineractLoanAccountNo", str(input, "fineractLoanAccountNo"));
|
||||||
}
|
}
|
||||||
|
holdDdsAccounts(applicationNo, app);
|
||||||
|
|
||||||
appendHistory(app, "DISBURSED", actor(input), "Disbursed", strOr(input, "remarks",
|
appendHistory(app, "DISBURSED", actor(input), "Disbursed", strOr(input, "remarks",
|
||||||
"Disbursed via " + mode + " — ACH confirmation " + strOr(app, "achConfirmationRef", "—") + ". EMI activation confirmed."));
|
"Disbursed via " + mode + " — ACH confirmation " + strOr(app, "achConfirmationRef", "—") + ". EMI activation confirmed."));
|
||||||
return pipelineService.storeApplication(applicationNo, app);
|
return pipelineService.storeApplication(applicationNo, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A loan against a Daily Deposit Scheme account puts the account on hold (its lien flag) as it is disbursed, naming the loan
|
||||||
|
* in the reason; {@link DdsLienReleaseService} lifts it when that loan ends. The DDS module is the bank's own, outside
|
||||||
|
* Fineract's deposit accounts, so there is no Fineract call for this. The outcome is written on the application's lien.
|
||||||
|
*/
|
||||||
|
private void holdDdsAccounts(final String applicationNo, final JsonObject app) {
|
||||||
|
if (!"DS".equals(str(app, "productCode"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final JsonObject selection = obj(app, "depositSelection");
|
||||||
|
final JsonObject lien = obj(app, "lien");
|
||||||
|
if (selection == null || lien == null || !bool(lien, "marked")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String loanNo = filled(app, "fineractLoanAccountNo") ? str(app, "fineractLoanAccountNo") : applicationNo;
|
||||||
|
final String reason = DdsLienReleaseService.reasonFor(loanNo, applicationNo);
|
||||||
|
int held = 0;
|
||||||
|
for (final JsonElement id : arr(selection, "accountIds")) {
|
||||||
|
held += jdbcTemplate.update("UPDATE m_dds_party SET lien = TRUE, lien_reason = ? WHERE id = ? AND status <> 'CLOSED'", reason,
|
||||||
|
id.getAsLong());
|
||||||
|
}
|
||||||
|
lien.addProperty("coreStatus", held > 0 ? "PLACED" : "FAILED");
|
||||||
|
lien.addProperty("coreNote", held > 0 ? "DDS account held: " + reason : "No open DDS account was found to hold.");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Shared checklist rules (mirror the Application Detail screen's gating) ─
|
// ── Shared checklist rules (mirror the Application Detail screen's gating) ─
|
||||||
|
|
||||||
private void requireFieldInvestigationComplete(final JsonObject app) {
|
private void requireFieldInvestigationComplete(final JsonObject app) {
|
||||||
@@ -577,8 +724,9 @@ public class LoanOriginationStageService {
|
|||||||
if (!depositLoan) {
|
if (!depositLoan) {
|
||||||
require(bool(app, "cicReportUploaded"), "The credit bureau report is not on file.");
|
require(bool(app, "cicReportUploaded"), "The credit bureau report is not on file.");
|
||||||
}
|
}
|
||||||
require(bool(app, "backgroundCheckDone"), "The background check has not been done.");
|
// On a loan against a deposit the deposit is the security: KYC and the deposit checks are enough, the background check is optional.
|
||||||
if (bool(app, "secured")) {
|
require(isDepositLoan(app) || bool(app, "backgroundCheckDone"), "The background check has not been done.");
|
||||||
|
if (bool(app, "secured") && !depositLoan) {
|
||||||
require(bool(app, "cersaiSearchDone"), "The CERSAI search has not been done.");
|
require(bool(app, "cersaiSearchDone"), "The CERSAI search has not been done.");
|
||||||
}
|
}
|
||||||
require(bool(app, "aadhaarVerified"), "Aadhaar has not been verified.");
|
require(bool(app, "aadhaarVerified"), "Aadhaar has not been verified.");
|
||||||
@@ -588,6 +736,12 @@ public class LoanOriginationStageService {
|
|||||||
requireFieldInvestigationComplete(app);
|
requireFieldInvestigationComplete(app);
|
||||||
}
|
}
|
||||||
require(depositLoan || bool(app, "managerMeetingDone"), "The manager meeting has not been recorded.");
|
require(depositLoan || bool(app, "managerMeetingDone"), "The manager meeting has not been recorded.");
|
||||||
|
if (depositLoan) {
|
||||||
|
// A loan against a deposit has no committee, risk or legal approval: this one Sanction step is all, and it makes the
|
||||||
|
// checks Risk would have made (KYC, the deposit covers the loan, the declarations). No resolution is needed.
|
||||||
|
requireRiskChecklistReady(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
require(!strOr(input, "resolutionNo", "").isBlank(), "A committee resolution number is required.");
|
require(!strOr(input, "resolutionNo", "").isBlank(), "A committee resolution number is required.");
|
||||||
require(!strOr(input, "resolutionDate", "").isBlank(), "A committee resolution date is required.");
|
require(!strOr(input, "resolutionDate", "").isBlank(), "A committee resolution date is required.");
|
||||||
}
|
}
|
||||||
@@ -609,15 +763,52 @@ public class LoanOriginationStageService {
|
|||||||
requireCheckVerified(app, "addressVerification", "Address verification");
|
requireCheckVerified(app, "addressVerification", "Address verification");
|
||||||
require(bool(app, "incomeProofSubmitted"), "Income proof has not been submitted.");
|
require(bool(app, "incomeProofSubmitted"), "Income proof has not been submitted.");
|
||||||
}
|
}
|
||||||
require(bool(app, "previousLoanDeclarationDone"), "The previous-loan declaration has not been taken.");
|
// A loan against a deposit takes no separate declaration: the deposit is the security.
|
||||||
if (bool(app, "secured")) {
|
require(depositLoan || bool(app, "previousLoanDeclarationDone"), "The previous-loan declaration has not been taken.");
|
||||||
|
if (bool(app, "secured") && !depositLoan) {
|
||||||
require(bool(app, "propertySearchReportDone"), "The property search report is not on file.");
|
require(bool(app, "propertySearchReportDone"), "The property search report is not on file.");
|
||||||
require(bool(app, "valuationReportConfirmed"), "The valuation report has not been confirmed.");
|
require(bool(app, "valuationReportConfirmed"), "The valuation report has not been confirmed.");
|
||||||
}
|
}
|
||||||
if (!depositLoan) {
|
if (!depositLoan) {
|
||||||
requireFieldInvestigationComplete(app);
|
requireFieldInvestigationComplete(app);
|
||||||
}
|
}
|
||||||
require(bool(app, "backgroundCheckDone"), "The background check has not been done.");
|
// On a loan against a deposit the deposit is the security: KYC and the deposit checks are enough, the background check is optional.
|
||||||
|
require(isDepositLoan(app) || bool(app, "backgroundCheckDone"), "The background check has not been done.");
|
||||||
|
requireGuarantorsNamed(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The guarantors an application has to name. A product that says how many (Product Creation) is held to that count: each
|
||||||
|
* guarantor an onboarded client, none twice, none the borrower, and a bank member unless the product says otherwise. An
|
||||||
|
* application raised before products said so keeps the officer's own Guarantor Required tick and its single guarantor.
|
||||||
|
*/
|
||||||
|
private void requireGuarantorsNamed(final JsonObject app) {
|
||||||
|
final JsonElement count = app.get("guarantorsRequiredCount");
|
||||||
|
if (count != null && count.isJsonPrimitive() && count.getAsJsonPrimitive().isNumber()) {
|
||||||
|
final int need = count.getAsInt();
|
||||||
|
if (need <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final boolean memberRequired = !app.has("guarantorMustBeMember") || bool(app, "guarantorMustBeMember");
|
||||||
|
final Long borrower = LoanOriginationJson.lng(app, "clientId");
|
||||||
|
final java.util.Set<Long> seen = new java.util.HashSet<>();
|
||||||
|
for (final JsonElement element : arr(app, "guarantors")) {
|
||||||
|
if (!element.isJsonObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final JsonObject guarantor = element.getAsJsonObject();
|
||||||
|
final Long clientId = LoanOriginationJson.lng(guarantor, "clientId");
|
||||||
|
if (clientId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
require(borrower == null || !borrower.equals(clientId), "The borrower cannot be their own guarantor.");
|
||||||
|
require(seen.add(clientId), "A guarantor has been named twice.");
|
||||||
|
require(!memberRequired || bool(guarantor, "hasShareAccount"),
|
||||||
|
strOr(guarantor, "name", "A guarantor") + " has no active share account: a guarantor must be a bank member.");
|
||||||
|
}
|
||||||
|
require(seen.size() >= need, "This product needs " + need + " guarantor(s); " + seen.size() + " named.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (bool(app, "guarantorRequired")) {
|
if (bool(app, "guarantorRequired")) {
|
||||||
// A guarantor at a co-operative bank has to be an existing member, not just a name: an
|
// A guarantor at a co-operative bank has to be an existing member, not just a name: an
|
||||||
// onboarded client id AND a live share account.
|
// onboarded client id AND a live share account.
|
||||||
|
|||||||
@@ -634,6 +634,8 @@
|
|||||||
<include file="parts/0605_add_gold_revaluation_rules.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/0606_add_gold_servicing_and_auction_rules.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0607_add_warehouse_receipt_product_fields.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0607_add_warehouse_receipt_product_fields.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0608_seed_priority_sector_category_code.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0609_seed_weaker_section_type_code.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. -->
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
<?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 priority sector lending categories, kept as a Code so Loan Origination and the loan account wizard read
|
||||||
|
the list from the system instead of a fixed list. Names are shown as they are. -->
|
||||||
|
<changeSet author="CBS" id="0608_seed_priority_sector_category_code">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_code (code_name, is_system_defined)
|
||||||
|
SELECT 'PrioritySectorCategory', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'PrioritySectorCategory');
|
||||||
|
|
||||||
|
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
|
||||||
|
SELECT c.id, v.code_value, v.order_position, true, false
|
||||||
|
FROM m_code c
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('Agriculture', 1),
|
||||||
|
('Micro, Small and Medium Enterprises', 2),
|
||||||
|
('Export Credit', 3),
|
||||||
|
('Education', 4),
|
||||||
|
('Housing', 5),
|
||||||
|
('Social Infrastructure', 6),
|
||||||
|
('Renewable Energy', 7),
|
||||||
|
('Others', 8)
|
||||||
|
) AS v(code_value, order_position)
|
||||||
|
WHERE c.code_name = 'PrioritySectorCategory'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_code_value cv
|
||||||
|
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
<?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">
|
||||||
|
|
||||||
|
<!-- Weaker sections of the RBI Priority Sector Lending Master Directions (para 17.1): an overlapping tag on a priority
|
||||||
|
sector loan, chosen at sanction in Loan Origination. Kept as a Code so the list can be maintained in Admin. -->
|
||||||
|
<changeSet author="CBS" id="0609_seed_weaker_section_type_code">
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_code (code_name, is_system_defined)
|
||||||
|
SELECT 'WeakerSectionType', false
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_code WHERE code_name = 'WeakerSectionType');
|
||||||
|
|
||||||
|
INSERT INTO m_code_value (code_id, code_value, order_position, is_active, is_mandatory)
|
||||||
|
SELECT c.id, v.code_value, v.order_position, true, false
|
||||||
|
FROM m_code c
|
||||||
|
CROSS JOIN (VALUES
|
||||||
|
('Small and Marginal Farmers', 1),
|
||||||
|
('Artisans, village and cottage industries (individual credit limit up to ₹2 lakh)', 2),
|
||||||
|
('Beneficiaries of Government Sponsored Schemes (NRLM, SRMS)', 3),
|
||||||
|
('Scheduled Castes and Scheduled Tribes', 4),
|
||||||
|
('Beneficiaries of Differential Rate of Interest (DRI) scheme', 5),
|
||||||
|
('Self Help Groups / Joint Liability Groups', 6),
|
||||||
|
('Individuals and members of SHGs/JLGs availing microfinance loans', 7),
|
||||||
|
('Individual women beneficiaries (up to ₹2 lakh)', 8),
|
||||||
|
('Distressed farmers indebted to non-institutional lenders', 9),
|
||||||
|
('Distressed persons other than farmers (up to ₹1 lakh)', 10),
|
||||||
|
('Persons with disabilities', 11),
|
||||||
|
('Transgenders', 12),
|
||||||
|
('Minority communities notified by Government of India', 13)
|
||||||
|
) AS v(code_value, order_position)
|
||||||
|
WHERE c.code_name = 'WeakerSectionType'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_code_value cv
|
||||||
|
WHERE cv.code_id = c.id AND cv.code_value = v.code_value
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
Reference in New Issue
Block a user