CoolFace
Apppublic

PABPAT/TCI_Shield

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
models.py181 linesDownload Raw Back to core
1# ============================================================2# TRADE CREDIT INSURANCE -- PYDANTIC DATA MODELS3# ============================================================4# Defines strict data models for each collection stage.5# Used to validate data before storing in session.6# If any field is missing or wrong type, validation fails7# and a clear error is returned to Nova.8# ============================================================9 10from pydantic import BaseModel, Field, field_validator, model_validator11from typing import Optional12import logging13 14# ============================================================15# SECTION 1 -- BUSINESS INFO MODEL16# ============================================================17 18class BusinessInfo(BaseModel):19    """20    Validates business information collected in Steps 1-5.21    All fields are required -- no optional fields.22    """23    business_name:            str   = Field(..., min_length=1,   description="Name of the business")24    industries:               list  = Field(..., min_length=1,   description="List of industry sectors")25    trade_type:               str   = Field(...,                 description="export, domestic, or both")26    annual_turnover:          float = Field(..., gt=0,           description="Annual turnover in GBP")27    credit_sales_percentage:  float = Field(..., gt=0, le=100,   description="Percentage of turnover sold on credit")28    years_in_business:        int   = Field(..., gt=0,           description="Years the business has been trading")29    customer_country:         str   = Field(..., min_length=2,   description="Country where business is based")30    customer_country_code:    str   = Field(..., min_length=2, max_length=3, description="2-letter country code")31 32    @field_validator("trade_type")33    @classmethod34    def validate_trade_type(cls, value: str) -> str:35        allowed = ["export", "domestic", "both"]36        if value.lower() not in allowed:37            raise ValueError(f"Trade type must be one of: {allowed}. Got: {value}")38        return value.lower()39 40    @field_validator("customer_country_code")41    @classmethod42    def validate_country_code(cls, value: str) -> str:43        return value.upper().strip()44 45    @field_validator("industries")46    @classmethod47    def validate_industries(cls, value: list) -> list:48        if not value or len(value) == 0:49            raise ValueError("At least one industry must be provided")50        return value51 52 53# ============================================================54# SECTION 2 -- BUYER MODEL55# ============================================================56 57class Buyer(BaseModel):58    """59    Validates a single buyer's details.60    All fields required except registration_number.61    """62    name:                str   = Field(..., min_length=1,  description="Buyer business name")63    country:             str   = Field(..., min_length=2,  description="Buyer country full name")64    country_code:        str   = Field(..., min_length=2, max_length=3, description="2-letter country code")65    industry:            str   = Field(..., min_length=2,  description="Buyer industry sector")66    exposure_amount:     float = Field(..., gt=0,          description="Credit exposure in GBP")67    registration_number: str   = Field("UNKNOWN",          description="Company registration number")68 69    @field_validator("country_code")70    @classmethod71    def validate_country_code(cls, value: str) -> str:72        return value.upper().strip()73 74 75# ============================================================76# SECTION 3 -- BUYER INFO MODEL77# ============================================================78 79class BuyerInfo(BaseModel):80    """81    Validates all buyer and trading information collected in Steps 7-11.82    """83    buyers:               list  = Field(..., min_length=1,        description="List of buyer objects")84    buyer_countries:      list  = Field(..., min_length=1,        description="List of all buyer countries")85    top_buyer_percentage: float = Field(..., gt=0, le=100,        description="% of exposure in largest buyer")86    payment_terms_days:   int   = Field(..., gt=0,                description="Standard payment terms in days")87    loss_ratio:           float = Field(..., ge=0, le=1,          description="Historical bad debt ratio as decimal")88    declared_buyer_count: int   = Field(..., gt=0,                description="Total buyers declared by customer")89 90    @model_validator(mode="after")91    def validate_buyer_count(self) -> "BuyerInfo":92        if len(self.buyers) != self.declared_buyer_count:93            raise ValueError(94                f"Buyer count mismatch -- declared {self.declared_buyer_count} "95                f"but {len(self.buyers)} provided. Collect all buyers before submitting."96            )97        return self98 99    @model_validator(mode="after")100    def validate_buyer_objects(self) -> "BuyerInfo":101        validated_buyers = []102        for i, buyer in enumerate(self.buyers, 1):103            if isinstance(buyer, dict):104                try:105                    validated_buyers.append(Buyer(**buyer).model_dump())106                except Exception as e:107                    raise ValueError(f"Buyer {i} validation failed: {e}")108            else:109                validated_buyers.append(buyer)110        self.buyers = validated_buyers111        return self112 113 114# ============================================================115# SECTION 4 -- FINANCIAL DATA MODEL116# ============================================================117 118class FinancialData(BaseModel):119    """120    Validates financial figures extracted from financial statements.121    All 11 figures required. TNW can be negative (insolvency indicator).122    """123    annual_revenue:       float = Field(..., gt=0,   description="Annual revenue in GBP")124    current_assets:       float = Field(..., gt=0,   description="Current assets in GBP")125    current_liabilities:  float = Field(..., gt=0,   description="Current liabilities in GBP")126    total_liabilities:    float = Field(..., gt=0,   description="Total liabilities in GBP")127    tangible_net_worth:   float = Field(...,          description="Tangible net worth -- can be negative")128    total_assets:         float = Field(..., gt=0,   description="Total assets in GBP")129    capital:              float = Field(..., ge=0,   description="Paid up capital in GBP")130    bad_debts:            float = Field(..., ge=0,   description="Bad debts written off in GBP")131    debtors:              float = Field(..., ge=0,   description="Total debtors in GBP")132    creditors:            float = Field(..., ge=0,   description="Total creditors in GBP")133    cost_of_sales:        float = Field(..., gt=0,   description="Cost of sales in GBP")134 135    @model_validator(mode="after")136    def validate_balance_sheet(self) -> "FinancialData":137        # Basic balance sheet sanity check138        # Total assets should roughly equal total liabilities + TNW139        expected_assets = self.total_liabilities + self.tangible_net_worth140        tolerance       = self.total_assets * 0.10  # 10% tolerance141        if abs(self.total_assets - expected_assets) > tolerance:142            logging.warning(143                f"Balance sheet may be inconsistent: "144                f"Total Assets={self.total_assets}, "145                f"Liabilities + TNW={expected_assets}"146            )147        return self148 149 150# ============================================================151# SECTION 5 -- VALIDATION HELPER152# ============================================================153 154def validate_model(model_class, data: dict) -> tuple[bool, str, dict]:155    """156    Validates data against a Pydantic model.157 158    Args:159        model_class : Pydantic model class to validate against160        data        : dict of data to validate161 162    Returns:163        tuple of (is_valid, error_message, validated_data)164        - is_valid       : True if validation passed165        - error_message  : empty string if valid, error details if invalid166        - validated_data : validated and cleaned data dict if valid, else empty167    """168    try:169        validated = model_class(**data)170        return True, "", validated.model_dump()171    except Exception as e:172        # Extract clean error messages from Pydantic ValidationError173        errors = []174        if hasattr(e, "errors"):175            for err in e.errors():176                field   = " -> ".join(str(f) for f in err.get("loc", []))177                message = err.get("msg", "Invalid value")178                errors.append(f"{field}: {message}")179        else:180            errors.append(str(e))181        return False, f"Validation failed: {'; '.join(errors)}", {}