| from decimal import Decimal |
|
|
| from sqlalchemy import select |
| from sqlalchemy.orm import Session |
|
|
| from app.core.enums import DecisionAction, RuleType |
| from app.models.rule import RuleMaster |
|
|
|
|
| DEFAULT_RULES = [ |
| { |
| "rule_code": "CIBIL_HIGH", |
| "rule_name": "High CIBIL score", |
| "rule_type": RuleType.SCORE, |
| "field_name": "cibilScore", |
| "operator": ">=", |
| "field_value": "750", |
| "score_value": Decimal("20"), |
| "score_component": "cibil", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "HIGH_CIBIL", |
| "priority": 100, |
| }, |
| { |
| "rule_code": "CIBIL_MEDIUM", |
| "rule_name": "Medium CIBIL score", |
| "rule_type": RuleType.SCORE, |
| "field_name": "cibilScore", |
| "operator": "BETWEEN", |
| "field_value": "700,749", |
| "score_value": Decimal("14"), |
| "score_component": "cibil", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "MEDIUM_CIBIL", |
| "priority": 110, |
| }, |
| { |
| "rule_code": "BUREAU_CLEAN", |
| "rule_name": "Clean repayment behaviour", |
| "rule_type": RuleType.SCORE, |
| "field_name": "noDpd", |
| "operator": "=", |
| "field_value": "true", |
| "score_value": Decimal("25"), |
| "score_component": "bureau", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "NO_DELINQUENCY", |
| "priority": 120, |
| }, |
| { |
| "rule_code": "FOIR_GOOD", |
| "rule_name": "Low FOIR", |
| "rule_type": RuleType.SCORE, |
| "field_name": "foir", |
| "operator": "<=", |
| "field_value": "35", |
| "score_value": Decimal("15"), |
| "score_component": "foir", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "LOW_FOIR", |
| "priority": 130, |
| }, |
| { |
| "rule_code": "FOIR_ACCEPTABLE", |
| "rule_name": "Acceptable FOIR", |
| "rule_type": RuleType.SCORE, |
| "field_name": "foir", |
| "operator": "BETWEEN", |
| "field_value": "36,50", |
| "score_value": Decimal("8"), |
| "score_component": "foir", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "ACCEPTABLE_FOIR", |
| "priority": 131, |
| }, |
| { |
| "rule_code": "LTV_GOOD", |
| "rule_name": "Low LTV", |
| "rule_type": RuleType.SCORE, |
| "field_name": "ltv", |
| "operator": "<=", |
| "field_value": "75", |
| "score_value": Decimal("10"), |
| "score_component": "ltv", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "LOW_LTV", |
| "priority": 140, |
| }, |
| { |
| "rule_code": "LTV_ACCEPTABLE", |
| "rule_name": "Acceptable LTV", |
| "rule_type": RuleType.SCORE, |
| "field_name": "ltv", |
| "operator": "BETWEEN", |
| "field_value": "76,85", |
| "score_value": Decimal("8"), |
| "score_component": "ltv", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "ACCEPTABLE_LTV", |
| "priority": 141, |
| }, |
| { |
| "rule_code": "NO_FRAUD", |
| "rule_name": "No fraud indicators", |
| "rule_type": RuleType.SCORE, |
| "field_name": "noFraud", |
| "operator": "=", |
| "field_value": "true", |
| "score_value": Decimal("10"), |
| "score_component": "fraud", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "NO_FRAUD", |
| "priority": 150, |
| }, |
| { |
| "rule_code": "STABLE_EMPLOYMENT", |
| "rule_name": "Stable employment type", |
| "rule_type": RuleType.SCORE, |
| "field_name": "stableEmployment", |
| "operator": "=", |
| "field_value": "true", |
| "score_value": Decimal("10"), |
| "score_component": "employment", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "STABLE_EMPLOYMENT", |
| "priority": 160, |
| }, |
| { |
| "rule_code": "BANKING_SURROGATE_OK", |
| "rule_name": "Banking surrogate available", |
| "rule_type": RuleType.SCORE, |
| "field_name": "bankingSurrogate", |
| "operator": "=", |
| "field_value": "true", |
| "score_value": Decimal("5"), |
| "score_component": "banking", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "BANKING_SURROGATE", |
| "priority": 170, |
| }, |
| { |
| "rule_code": "KYC_VERIFIED", |
| "rule_name": "PAN and Aadhaar verified", |
| "rule_type": RuleType.SCORE, |
| "field_name": "kycVerified", |
| "operator": "=", |
| "field_value": "true", |
| "score_value": Decimal("5"), |
| "score_component": "kyc", |
| "decision_action": DecisionAction.SCORE, |
| "reason_code": "KYC_VERIFIED", |
| "priority": 180, |
| }, |
| { |
| "rule_code": "WRITTEN_OFF_REJECT", |
| "rule_name": "Written off account reject", |
| "rule_type": RuleType.HARD_REJECT, |
| "field_name": "writtenOffAccounts", |
| "operator": ">", |
| "field_value": "0", |
| "decision_action": DecisionAction.REJECT, |
| "reason_code": "WRITTEN_OFF_ACCOUNT", |
| "priority": 10, |
| }, |
| { |
| "rule_code": "SETTLED_REJECT", |
| "rule_name": "Settled account reject", |
| "rule_type": RuleType.HARD_REJECT, |
| "field_name": "settledAccounts", |
| "operator": ">", |
| "field_value": "0", |
| "decision_action": DecisionAction.REJECT, |
| "reason_code": "SETTLED_ACCOUNT", |
| "priority": 11, |
| }, |
| { |
| "rule_code": "OVERDUE_REJECT", |
| "rule_name": "Current overdue above threshold", |
| "rule_type": RuleType.HARD_REJECT, |
| "field_name": "overdueAmount", |
| "operator": ">", |
| "field_value": "5000", |
| "decision_action": DecisionAction.REJECT, |
| "reason_code": "CURRENT_OVERDUE", |
| "priority": 12, |
| }, |
| { |
| "rule_code": "FRAUD_REJECT", |
| "rule_name": "Fraud flag reject", |
| "rule_type": RuleType.HARD_REJECT, |
| "field_name": "fraudFlag", |
| "operator": "=", |
| "field_value": "true", |
| "decision_action": DecisionAction.REJECT, |
| "reason_code": "FRAUD_FLAG", |
| "priority": 13, |
| }, |
| { |
| "rule_code": "PAN_MISMATCH_REJECT", |
| "rule_name": "PAN mismatch reject", |
| "rule_type": RuleType.HARD_REJECT, |
| "field_name": "panMismatch", |
| "operator": "=", |
| "field_value": "true", |
| "decision_action": DecisionAction.REJECT, |
| "reason_code": "PAN_MISMATCH", |
| "priority": 14, |
| }, |
| { |
| "rule_code": "DPD_90_REJECT", |
| "rule_name": "90 plus DPD reject", |
| "rule_type": RuleType.HARD_REJECT, |
| "field_name": "active90PlusDpd", |
| "operator": "=", |
| "field_value": "true", |
| "decision_action": DecisionAction.REJECT, |
| "reason_code": "ACTIVE_90_PLUS_DPD", |
| "priority": 15, |
| }, |
| { |
| "rule_code": "MAX_DPD_UW", |
| "rule_name": "Max DPD above STP threshold", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "maxDPD", |
| "operator": ">", |
| "field_value": "30", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "MAX_DPD_ABOVE_30", |
| "priority": 200, |
| }, |
| { |
| "rule_code": "DPD_60_UW", |
| "rule_name": "60 DPD history", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "dpd60Count", |
| "operator": ">", |
| "field_value": "0", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "DPD_60_HISTORY", |
| "priority": 201, |
| }, |
| { |
| "rule_code": "DPD_90_UW", |
| "rule_name": "90 DPD history", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "dpd90Count", |
| "operator": ">", |
| "field_value": "0", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "DPD_90_HISTORY", |
| "priority": 202, |
| }, |
| { |
| "rule_code": "ENQUIRY_HIGH_UW", |
| "rule_name": "High enquiry burst", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "creditEnquiries6M", |
| "operator": ">", |
| "field_value": "5", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "HIGH_ENQUIRY_BURST", |
| "priority": 203, |
| }, |
| { |
| "rule_code": "FOIR_HIGH_UW", |
| "rule_name": "High FOIR underwriting", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "foir", |
| "operator": ">", |
| "field_value": "50", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "HIGH_FOIR", |
| "priority": 204, |
| }, |
| { |
| "rule_code": "UNSECURED_LOANS_UW", |
| "rule_name": "Multiple active unsecured loans", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "activeUnsecuredLoans", |
| "operator": ">", |
| "field_value": "1", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "MULTIPLE_UNSECURED_LOANS", |
| "priority": 205, |
| }, |
| { |
| "rule_code": "FACE_MATCH_LOW_UW", |
| "rule_name": "Low face match", |
| "rule_type": RuleType.NON_STP, |
| "field_name": "faceMatchScore", |
| "operator": "<", |
| "field_value": "75", |
| "decision_action": DecisionAction.UW, |
| "reason_code": "LOW_FACE_MATCH", |
| "priority": 206, |
| }, |
| { |
| "rule_code": "STP_CIBIL", |
| "rule_name": "STP CIBIL threshold", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "cibilScore", |
| "operator": ">=", |
| "field_value": "750", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_CIBIL_OK", |
| "priority": 300, |
| }, |
| { |
| "rule_code": "STP_NO_WRITEOFF", |
| "rule_name": "STP no written off", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "writtenOffAccounts", |
| "operator": "=", |
| "field_value": "0", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_NO_WRITEOFF", |
| "priority": 301, |
| }, |
| { |
| "rule_code": "STP_NO_SETTLEMENT", |
| "rule_name": "STP no settlement", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "settledAccounts", |
| "operator": "=", |
| "field_value": "0", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_NO_SETTLEMENT", |
| "priority": 302, |
| }, |
| { |
| "rule_code": "STP_NO_OVERDUE", |
| "rule_name": "STP no overdue", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "overdueAmount", |
| "operator": "=", |
| "field_value": "0", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_NO_OVERDUE", |
| "priority": 303, |
| }, |
| { |
| "rule_code": "STP_MAX_DPD", |
| "rule_name": "STP max DPD threshold", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "maxDPD", |
| "operator": "<=", |
| "field_value": "30", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_DPD_OK", |
| "priority": 304, |
| }, |
| { |
| "rule_code": "STP_FOIR", |
| "rule_name": "STP FOIR threshold", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "foir", |
| "operator": "<=", |
| "field_value": "40", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_FOIR_OK", |
| "priority": 305, |
| }, |
| { |
| "rule_code": "STP_ENQUIRY", |
| "rule_name": "STP enquiry threshold", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "creditEnquiries6M", |
| "operator": "<=", |
| "field_value": "3", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_ENQUIRY_OK", |
| "priority": 306, |
| }, |
| { |
| "rule_code": "STP_FACE_MATCH", |
| "rule_name": "STP face match threshold", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "faceMatchScore", |
| "operator": ">=", |
| "field_value": "85", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_FACE_MATCH_OK", |
| "priority": 307, |
| }, |
| { |
| "rule_code": "STP_KYC", |
| "rule_name": "STP KYC verified", |
| "rule_type": RuleType.STP_ELIGIBILITY, |
| "field_name": "kycVerified", |
| "operator": "=", |
| "field_value": "true", |
| "decision_action": DecisionAction.STP, |
| "reason_code": "STP_KYC_OK", |
| "priority": 308, |
| }, |
| ] |
|
|
| SUPPORTED_PRODUCT_TYPES = [ |
| "TWO_WHEELER_LOAN", |
| "NEW_CAR_LOAN", |
| "USED_CAR_LOAN", |
| "NEW_COMMERCIAL_VEHICLE_LOAN", |
| "USED_COMMERCIAL_VEHICLE_LOAN", |
| "NEW_TRACTOR_LOAN", |
| "USED_TRACTOR_LOAN", |
| ] |
|
|
|
|
| def seed_default_rules( |
| db: Session, tenant_id: str = "default", product_type: str = "TWO_WHEELER_LOAN" |
| ) -> int: |
| inserted = 0 |
| for item in DEFAULT_RULES: |
| exists = db.scalar( |
| select(RuleMaster.id).where( |
| RuleMaster.tenant_id == tenant_id, |
| RuleMaster.product_type == product_type, |
| RuleMaster.rule_code == item["rule_code"], |
| RuleMaster.version == 1, |
| RuleMaster.dealer_id.is_(None), |
| ) |
| ) |
| if exists: |
| continue |
| db.add( |
| RuleMaster( |
| tenant_id=tenant_id, |
| product_type=product_type, |
| dealer_id=None, |
| version=1, |
| is_active=True, |
| created_by="seed", |
| **item, |
| ) |
| ) |
| inserted += 1 |
| return inserted |
|
|
|
|
| def seed_all_product_rules(db: Session, tenant_id: str = "default") -> int: |
| inserted = 0 |
| for product_type in SUPPORTED_PRODUCT_TYPES: |
| inserted += seed_default_rules(db, tenant_id=tenant_id, product_type=product_type) |
| return inserted |
|
|