Spaces:
Runtime error
Runtime error
| 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 | |