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