diff --git a/README.md b/README.md index 59a3efc..92008a8 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -**Hello world!!!** +## Advanced M series V1 model block + +M Series Model trained on historical data to identify fraudulent patterns. \ No newline at end of file diff --git a/block.py b/block.py index 3b227f9..5da574a 100644 --- a/block.py +++ b/block.py @@ -1,21 +1,174 @@ -@flowx_block -def example_function(request: dict) -> dict: +import pandas as pd +import logging +import json +import jmespath +import regex as re +from pre_processing import pre_processing +from processing import processing +from post_processing import post_processing - # Processing logic here... +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", +) +logger = logging.getLogger(__name__) - return { - "meta_info": [ - { - "name": "created_date", - "type": "string", - "value": "2024-11-05" - } + +def extract_value(blob, expression): + try: + return jmespath.search(expression, blob) + except Exception: + return None + +# Coalesce function to return the first non-None value +def coalesce(*args): + for value in args: + if value is not None: + return value + return None + +# New sanitize blob function +def sanitize_blob(blob): + try: + blob = re.sub(r'"(\w+)":"(\{[^}]+\})"', r'"\1":\2', blob) + blob = re.sub(r'"tps_vendor_raw_response"\s*:\s*"\?\{', '"tps_vendor_raw_response":{', blob) + blob = blob.replace('\\"', '"') + blob = blob.replace('\\n', '') + blob = blob.replace('\\t', '') + blob = blob.replace('\\\\', '') + blob = re.sub(r'(\}\})"', r'\1', blob) + blob = re.sub(r',\s*([\}\]])', r'\1', blob) + return json.loads(blob) + except json.JSONDecodeError as e: + logger.error(f"JSON Decode Error: {e}") + error_pos = e.pos + snippet = blob[max(0, error_pos - 50): error_pos + 50] + logger.error(f"Error near:\n{snippet}") + return None +#---------------- Sanitise ends here + +# Function to extract a value using JMESPath +# Expressions to extract values +expressions = { + "first_seen_days": [ + "tps_vendor_raw_response.query.results[0].first_seen_days", + "emailage.emailriskscore.first_seen_days" ], - "fields": [ - { - "name": "", - "type": "", - "value": "" - } - ] - } + "ea_score": [ + "tps_vendor_raw_response.query.results[0].EAScore", + "emailage.emailriskscore.eascore" + ], + "email_creation_days": [ + "tps_vendor_raw_response.query.results[0].email_creation_days" + ], + "summary_risk_score": ["summary_risk_score"], + "digital_id_trust_score_rating": ["digital_id_trust_score_rating"], + "os_version": ["os_version"], + "account_email_worst_score": ["account_email_worst_score"], + "true_ip_score": ["true_ip_score"], + "ip_net_speed_cell": [ + "tps_vendor_raw_response.query.results[0].ip_netSpeedCell", + # "true_ip_connection_type" + ], + "account_email_score": ["account_email_score"], + "true_ip_worst_score": ["true_ip_worst_score"], + "proxy_ip_worst_score": ["proxy_ip_worst_score"], + "proxy_ip_score": ["proxy_ip_score"], + "fuzzy_device_score": ["fuzzy_device_score"], + "ip_region_confidence": ["tps_vendor_raw_response.query.results[0].ip_regionconf"], + "true_ip_state_confidence": ["true_ip_state_confidence"], + "fuzzy_device_worst_score": ["fuzzy_device_worst_score"], + "digital_id_confidence_rating": ["digital_id_confidence_rating"] +} + + +def __main__( + #Application-> + application_key: str, + application_timestamp: str, + application_ssn : str, + application_email_address: str, + application_bank_account_number: str, + application_is_rejected: str, + application_date_of_birth: str, + #uprovaloanapplication-> + educationlevel:str, + employmentstatus: str, + lengthatbank: str, + lengthatjob: str, + ownhome: str, + payfrequency: str, + monthsatresidence: str, + #thxresponse-> + EventType: str, + DigitalIdConfidence: str, + RiskRating: str, + TmxSummaryReasonCode: str, + TrueIpGeo: str, + Blob:str, + DeviceId:str, + FuzzyDeviceId: str + ) -> dict: + + # Convert input parameters into a flat dictionary + data = { + "application_key" : application_key, + "application_timestamp" : application_timestamp, + "application_ssn " : application_ssn , + "application_email_address" : application_email_address, + "application_bank_account_number" : application_bank_account_number, + "application_is_rejected" : application_is_rejected, + "application_date_of_birth" : application_date_of_birth, + "educationlevel" : educationlevel, + "employmentstatus" : employmentstatus, + "lengthatbank" : lengthatbank, + "lengthatjob" : lengthatjob, + "ownhome" : ownhome, + "payfrequency" : payfrequency, + "monthsatresidence" : monthsatresidence, + "EventType" : EventType, + "DigitalIdConfidence" : DigitalIdConfidence, + "RiskRating" : RiskRating, + "TmxSummaryReasonCode" : TmxSummaryReasonCode, + "TrueIpGeo" : TrueIpGeo, + "Blob" : Blob, + "DeviceId" : DeviceId, + "FuzzyDeviceId" : FuzzyDeviceId + } + + # Convert dictionary to a single-row DataFrame + combined_df = pd.DataFrame([data]) + combined_df.columns = combined_df.columns.str.lower() + combined_df["application_email_address"] = combined_df["application_email_address"].str.lower() + if Blob: + combined_df["blob"] = combined_df["blob"].apply(sanitize_blob) + + # Step 2: Extract values using the expressions dictionary + for column, expressions_list in expressions.items(): + combined_df[column] = combined_df["blob"].apply(lambda x: coalesce(*[extract_value(x, expr) for expr in expressions_list])) + + logger.info("pre_flowx data") + logger.info(combined_df.iloc[0].drop('blob').to_dict()) + else: + for column, expressions_list in expressions.items(): + combined_df[column] = None + logger.info("pre_flowx data") + logger.info(combined_df.iloc[0].to_dict()) + pre_processed_data = pre_processing(combined_df) + # logger.info(f"pre_processed_data: {pre_processed_data}") + logger.info("pre_processed data") + logger.info(pre_processed_data.iloc[0].to_dict()) + df = processing(pre_processed_data) + logger.info("procesed_data") + logger.info(df.iloc[0].to_dict()) + df["application_timestamp"] = df["application_timestamp"].astype(str) + # logger.info("prediction: %.8f", float(df['prediction'].iloc[0])) + result = post_processing(df) + # logger.info("Score: %.0f", float(result["hd_score_m1"])) + logger.info(result) + + return result + +# testing : +# __main__ diff --git a/category_orders_train.json b/category_orders_train.json new file mode 100644 index 0000000..61818df --- /dev/null +++ b/category_orders_train.json @@ -0,0 +1,88 @@ +{ +"employmentstatus": [ + "disability", + "fixed income", + "full time employed", + "other", + "part time employment", + "retired benefits", + "self employed", + "student", + "unemployed", + "welfare" + ], + "TrueIpGeo": [ + "other", + "us" + ], + "digital_id_trust_score_rating": [ + "high", + "low", + "neutral", + "very_high", + "very_low" + ], + "educationlevel": [ + "associate's degree", + "bachelor's degree", + "doctorate", + "high school", + "master's degree", + "other" + ], + "os_version": [ + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "8", + "9", + "unknown" + ], + "ip_net_speed_cell": [ + "broadband", + "cable", + "dialup", + "dsl", + "fixed wireless", + "mobile", + "mobile wireless", + "ocx", + "satellite", + "t1", + "tx", + "wireless", + "xdsl" + ], + "day_night": [ + "Day", + "Night" + ], + "digital_id_confidence_rating": [ + "high", + "medium", + "very_high", + "very_low" + ], + "RiskRating": [ + "high", + "low", + "medium", + "neutral", + "trusted" + ], + "payfrequency": [ + "biweekly", + "semimonthly" + ], + "ownhome": [ + "false", + "true" + ] + +} \ No newline at end of file diff --git a/post_processing.py b/post_processing.py new file mode 100644 index 0000000..ca0bd93 --- /dev/null +++ b/post_processing.py @@ -0,0 +1,25 @@ +import logging +import numpy as np + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +def post_processing(df): + + try: + df['hd_score_m1'] = np.round( + np.minimum(df['prediction'] * 100 + 0.00001, 1) * 85 + + np.maximum(np.log2(df['prediction'] * 100 + 0.000001) * 185, 0), + 0 + ) + logging.info(f"hd_score_m1 calculated: {df['hd_score_m1'].iloc[0]}") + except Exception as e: + logging.error(f"Error processing hd_score_m1 calculations: {e}") + + + return df[['application_key', 'application_timestamp', 'deviceid', 'fuzzydeviceid', 'application_email_address', 'hd_score_m1']].iloc[0].to_dict() diff --git a/pre_processing.py b/pre_processing.py new file mode 100644 index 0000000..29712b6 --- /dev/null +++ b/pre_processing.py @@ -0,0 +1,254 @@ +import pandas as pd +import numpy as np +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +def pre_processing(data_df): + + # combined_df = pd.DataFrame([input_data]) + # data = pd.DataFrame(data) + combined_df = data_df + combined_df["applicant_age"] = combined_df.apply(lambda row: pd.to_datetime(row["application_timestamp"]).year - pd.to_datetime(row["application_date_of_birth"]).year if pd.notnull(row["application_timestamp"]) and pd.notnull(row["application_date_of_birth"]) else None,axis=1 + ) + + # Extracting Temporal features + combined_df['application_timestamp'] = pd.to_datetime(combined_df["application_timestamp"]) + combined_df.loc[:, 'application_time'] = pd.to_datetime(combined_df['application_timestamp']).dt.time + + combined_df['day'] = combined_df['application_timestamp'].dt.day + combined_df['day_of_week'] = combined_df['application_timestamp'].dt.weekday # 0=Monday, 6=Sunday + + combined_df['day_sin'] = np.sin(2 * np.pi * combined_df['day'] / 31) + combined_df['day_cos'] = np.cos(2 * np.pi * combined_df['day'] / 31) + combined_df['day_of_week_sin'] = np.sin(2 * np.pi * combined_df['day_of_week'] / 7) + combined_df['day_of_week_cos'] = np.cos(2 * np.pi * combined_df['day_of_week'] / 7) + + # combined_df['is_weekend'] = combined_df['day_of_week'].apply(lambda x: 1 if x >= 5 else 0) + + # Create a day/night variable + def classify_day_night(hour): + if 6 <= hour < 18: + return 'Day' + else: + return 'Night' + + # Extract hour from application_time + combined_df['hour'] = combined_df['application_time'].apply(lambda x: x.hour if pd.notnull(x) else np.nan) + combined_df['day_night'] = combined_df['hour'].apply(lambda hour: classify_day_night(hour) if pd.notnull(hour) else 'Unknown') + + # combined_df['os_version'] = combined_df['os_version'].str.replace(r'[^a-zA-Z0-9]', '_', regex=True) + combined_df['os_version'] = combined_df['os_version'].apply(lambda x: x.split('.')[0] if isinstance(x, str) and '.' in x + else x.split('_')[0] if isinstance(x, str) and '_' in x + else x) + + + # Datatype conversions + # combined_df['Level_1_Link_Accept'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('Level_1_Link_Accept', na=False, regex=True).astype(int) + combined_df['Identity_Negative_History'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('Identity_Negative_History', na=False, regex=True).astype(int) + combined_df['Device_Negative_History'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('Device_Negative_History', na=False, regex=True).astype(int) + combined_df['Level_1_Link_Reject'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('Level_1_Link_Reject', na=False, regex=True).astype(int) + combined_df['IP_Negative_History'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('IP_Negative_History', na=False, regex=True).astype(int) + combined_df['Identity_Spoofing'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('Identity_Spoofing', na=False, regex=True).astype(int) + # combined_df['Bot'] = combined_df['tmxsummaryreasoncode'].astype(str).str.contains('Bot', na=False, regex=True).astype(int) + + combined_df['digitalidconfidence'] = pd.to_numeric(combined_df['digitalidconfidence'], errors='coerce').astype('Int64') + + # Rename Columns if Required + combined_df.rename(columns={ + 'DigitalIdConfidence': 'digitalidconfidence', + # 'inputipaddress_consistency': 'inputip_consistency', + # 'requestid_consistency': 'request_consistency', + # Add others as required if present in your DataFrame and needing renaming. + }, inplace=True) + + # #Testing : remove below + # combined_df.to_csv('op-pre-processing_intermediate.csv', index=False) + + dtype_dict = { + "applicant_age" : int, + "digitalidconfidence" : float, + "first_seen_days" : float, + "employmentstatus" : str, + "ea_score" : float, + "trueipgeo" : str, + "hour" : int, + "email_creation_days" : float, + "lengthatjob" : float, + "day_cos" : float, + "summary_risk_score" : float, + "digital_id_trust_score_rating" : str, + "day" : 'int32', + "lengthatbank" : float, + "day_of_week_cos" : float, + "Level_1_Link_Reject" : int, + "Identity_Negative_History" : int, + "educationlevel" : str, + "os_version" : str, + "account_email_worst_score" : float, + "true_ip_score" : float, + "ip_net_speed_cell" : str, + "account_email_score" : float, + "day_of_week" : 'int32', + "true_ip_worst_score" : float, + "proxy_ip_worst_score" : float, + "day_night" : str, + "proxy_ip_score" : float, + "monthsatresidence" : float, + "Device_Negative_History" : int, + "fuzzy_device_score" : float, + "day_sin" : float, + "ip_region_confidence" : float, + "true_ip_state_confidence" : float, + "IP_Negative_History" : int, + "fuzzy_device_worst_score" : float, + "digital_id_confidence_rating" : str, + "day_of_week_sin" : float, + "riskrating" : str, + "payfrequency" : str, + "ownhome" : str, + "Identity_Spoofing" : int + } + + next_block_cols = ['application_key', 'application_timestamp', 'deviceid', 'fuzzydeviceid', 'application_email_address'] + cols_to_keep = [col for col in dtype_dict.keys() if col in combined_df.columns] + + final_cols = list(set(next_block_cols).union(set(cols_to_keep))) + # Type casting + for col, dtype in dtype_dict.items(): + if col in combined_df.columns: + if dtype == int: + combined_df[col] = pd.to_numeric(combined_df[col], errors='coerce', downcast='integer') + elif dtype == float: + combined_df[col] = pd.to_numeric(combined_df[col], errors='coerce', downcast='float') + elif dtype == str: + combined_df[col] = combined_df[col].astype(str) + # cross check data type + capping_dict = { + "applicant_age": (18, 93), + "digitalidconfidence": (0, 9017), + "first_seen_days": (0, 10486), + "ea_score": (1, 930), + "hour": (0, 23), + "email_creation_days": (2438, 9661), + "lengthatjob": (1, 24), + "day_cos": (-0.9948693234, 1), + "summary_risk_score": (-100, 30), + "day": (1, 31), + "lengthatbank": (0, 25), + "day_of_week_cos": (-0.9009688679, 1), + "Level_1_Link_Reject": (0, 1), + "Identity_Negative_History": (0, 1), + "account_email_worst_score": (-52, 0), + "true_ip_score": (-38, 49), + "account_email_score": (-18, 9), + "day_of_week": (0, 6), + "true_ip_worst_score": (-100, 0), + "proxy_ip_worst_score": (-100, 0), + "proxy_ip_score": (-29, 60), + "monthsatresidence": (0, 25), + "Device_Negative_History": (0, 1), + "fuzzy_device_score": (-29, 14), + "day_sin": (-0.9987165072, 0.9987165072), + "ip_region_confidence": (75, 99), + # "true_ip_state_confidence": (5, 98), + "IP_Negative_History": (0, 1), + "fuzzy_device_worst_score": (-100, 0), + "day_of_week_sin": (-0.9749279122, 0.9749279122), + "Identity_Spoofing": (0, 1), + } + + + + # Apply capping + for column, (cap_min, cap_max) in capping_dict.items(): + if column in combined_df.columns: + combined_df[column] = combined_df[column].clip(lower=cap_min, upper=cap_max) + + + def handle_unknowns(X, column, known_values, default_treatment=None): + if column not in X.columns: + return X # Return X to avoid NoneType error + known_values = {str(val).lower() for val in known_values} + invalid_values = {None, "none", "nan", pd.NA} + X[column] = X[column].apply( + lambda x: str(x).lower() if pd.notna(x) and str(x).lower() in known_values + else (default_treatment if pd.notna(x) and str(x).lower() not in invalid_values else np.nan) + ) + return X # Always return the DataFrame + + + + + unknown_treatments = { + "employmentstatus": { + "valid_values": [ + "disability", "fixed income", "full time employed", "part time employment", + "retired benefits", "self employed", "student", "unemployed", "welfare" + ], + "default_treatment": "other" + }, + "trueipgeo": { + "valid_values": ["US"], + "default_treatment": "other" + }, + "digital_id_trust_score_rating": { + "valid_values": ["very_high", "high", "neutral", "low"], + "default_treatment": "very_low" + }, + "educationlevel": { + "valid_values": ["associate's degree", "bachelor's degree", "doctorate", "high school", "master's degree"], + "default_treatment": "other" + }, + "os_version": { + "valid_values": [ + '18', '17', '16', '15', '14', '13', '12', '11', '10', '9', '8' + ], + "default_treatment": 'unknown' + }, + "ip_net_speed_cell": { + "valid_values": [ + "broadband", "cable", "dialup", "dsl", "fixed wireless", "mobile", "mobile wireless", "ocx", "satellite", + "t1", "tx", "wireless", "xdsl" + ], + "default_treatment": "mobile" + }, + "digital_id_confidence_rating": { + "valid_values": ["high", "medium", "very_high"], + "default_treatment": "very_low" + }, + "riskrating": { + "valid_values": ["low", "medium", "neutral", "trusted"], + "default_treatment": "high" + }, + "ownhome": { + "valid_values": ["true", "false"], + "default_treatment": np.nan + }, + } + + for column, treatment in unknown_treatments.items(): + combined_df = handle_unknowns(combined_df, column, treatment["valid_values"], treatment["default_treatment"]) + + payfrequency_map = { + "biweekly": ["biweekly", "bi-weekly", "bi weekly", "bw"], + "semimonthly": ["semi-monthly", "semimonthly"] + } + + combined_df['payfrequency'] = combined_df['payfrequency'].apply( + lambda x: next((key for key, values in payfrequency_map.items() if str(x).lower() in values), np.nan) + ) + + return combined_df[final_cols] + + + + + + diff --git a/processing.py b/processing.py new file mode 100644 index 0000000..45b23c1 --- /dev/null +++ b/processing.py @@ -0,0 +1,46 @@ +import pandas as pd +import numpy as np +import xgboost as xgb +import joblib +import json + + +def processing(input_data): + df = pd.DataFrame(input_data) + + # Load Model + model_path = "./xgboost_model.joblib" + # model_path = "C:/Users/abinisha/habemco_flowx/m1_v1/xgboost_model.joblib" + model = joblib.load(model_path) + df.rename(columns={'riskrating': 'RiskRating', 'trueipgeo': 'TrueIpGeo'}, inplace=True) + + # Load Category Orders + category_orders_path ="./category_orders_train.json" + # category_orders_path = "C:/Users/abinisha/habemco_flowx/m1_v1/category_orders_train.json" + with open(category_orders_path, 'r') as f: + category_orders = json.load(f) + + if df.empty: + raise ValueError("Input DataFrame is empty.") + + + # Ensure all expected features exist + expected_features = model.feature_names + + + for col, categories in category_orders.items(): + df[col].replace([None, "", "null", np.nan, "nan", " "], np.nan, inplace=True) + df[col] = pd.Categorical(df[col], categories=categories, ordered=True) + + # missing_features = [feature for feature in expected_features if feature not in df.columns] + # for feature in missing_features: + # df[feature] = np.nan # Use NaN to avoid dtype issues + + # Create XGBoost DMatrix + dmatrix = xgb.DMatrix(df[expected_features], enable_categorical=True, missing=np.nan) + + # Make predictions + predictions = model.predict(dmatrix) + df['prediction'] = predictions + + return df diff --git a/request_schema.json b/request_schema.json index 0967ef4..73d1e51 100644 --- a/request_schema.json +++ b/request_schema.json @@ -1 +1,95 @@ -{} +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "application_key": { + "type": ["string", "null"], + "description": "Unique identifier for the application." + }, + "application_timestamp": { + "type": ["string", "null"], + "description": "Timestamp when the application was submitted in UTC." + }, + "application_ssn": { + "type": ["string", "null"], + "description": "Social Security Number of the applicant." + }, + "application_email_address": { + "type": ["string", "null"], + "description": "Email address of the applicant." + }, + "application_bank_account_number": { + "type": ["string", "null"], + "description": "Bank account number of the applicant." + }, + "application_is_rejected": { + "type": ["boolean", "null"], + "description": "Indicates whether the application was rejected." + }, + "application_date_of_birth": { + "type": ["string", "null"], + "description": "Date of birth of the applicant." + }, + "EventType": { + "type": ["string", "null"], + "description": "Type of event associated with the application." + }, + "RiskRating": { + "type": ["string", "null"], + "description": "Risk rating assigned to the application." + }, + "TmxSummaryReasonCode": { + "type": ["string", "null"], + "description": "Reason code summary from third-party risk assessment." + }, + "DigitalIdConfidence": { + "type": ["string", "null"], + "description": "Confidence score for the digital identity of the applicant." + }, + "TrueIpGeo": { + "type": ["string", "null"], + "description": "Geolocation information of the true IP address used in the application." + }, + "Blob": { + "type": ["string", "null"], + "description": "Raw data blob containing additional information related to the application." + }, + "DeviceId": { + "type": ["string", "null"], + "description": "Unique identifier for the device used to submit the application." + }, + "FuzzyDeviceId": { + "type": ["string", "null"], + "description": "Hashed or partially anonymized identifier for the device." + }, + "ownhome": { + "type": ["boolean", "null"], + "description": "Indicates whether the applicant owns a home." + }, + "employmentstatus": { + "type": ["string", "null"], + "description": "Employment status of the applicant." + }, + "lengthatjob": { + "type": ["number", "null"], + "description": "Length of time (in months) the applicant has been at their current job." + }, + "payfrequency": { + "type": ["string", "null"], + "description": "Frequency of pay for the applicant (e.g., weekly, biweekly, monthly)." + }, + "lengthatbank": { + "type": ["string", "null"], + "description": "Length of time the applicant has been with their bank." + }, + "educationlevel": { + "type": ["string", "null"], + "description": "Highest level of education attained by the applicant." + }, + "monthsatresidence": { + "type": ["number", "null"], + "description": "Number of months the applicant has lived at their current residence." + } + }, + "required": [] +} diff --git a/requirements.txt b/requirements.txt index 0967ef4..049e4db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,6 @@ -{} +pandas == 2.2.3 +numpy == 2.2.3 +xgboost == 2.1.4 +joblib == 1.4.2 +jmespath == 1.0.1 +regex == 2023.12.25 diff --git a/response_schema.json b/response_schema.json index 0967ef4..e03dd58 100644 --- a/response_schema.json +++ b/response_schema.json @@ -1 +1,34 @@ -{} +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "application_key": { + "type": ["string", "null"], + "description": "Application Key" + }, + "application_timestamp": { + "type": ["string", "null"], + "description": "Application Timestamp" + }, + "deviceid": { + "type": ["string", "null"], + "description": "Deviceid" + }, + "fuzzydeviceid": { + "type": ["string", "null"], + "description": "Fuzzy Deviceid" + }, + "application_email_address": { + "type": ["string", "null"], + "description": "Application Email Address" + }, + "hd_score_m1": { + "type": ["number", "null"], + "description": "HD Fraud Score M1" + }, + "action": { + "type": ["string", "null"], + "description": "Recommended Action." + } + } +} diff --git a/test_block.py b/test_block.py new file mode 100644 index 0000000..44a968a --- /dev/null +++ b/test_block.py @@ -0,0 +1,45 @@ +# Working test_block.py +import unittest +import pandas as pd +from block import __main__ + + +input_json = { + "application_key": "7DE3C940-7747-4EA8-B25F-3276BA05BA71", + "application_timestamp": "2024-12-25 15:20:45", + "application_ssn": "274728658", + "application_email_address": "kralldad40@gmail.com", + "application_bank_account_number": "7926569125.0", + "application_is_rejected": 'true', + "application_date_of_birth": "1976-08-19 00:00:00", + "EventType": "account_creation", + "RiskRating": "neutral", + "TmxSummaryReasonCode": "Level_1_Link_Accept", + "DigitalIdConfidence": "7554.0", + "TrueIpGeo": "US", + "Blob": """{\"account_address_city\":\"hamilton\",\"account_address_state\":\"oh\",\"account_address_street1\":\"100 horizon driv\",\"account_address_zip\":\"45013\",\"account_date_of_birth\":\"19760819\",\"account_email\":\"kralldad40@gmail.com\",\"account_email_activities\":[\"_TRUSTED_PROB\"],\"account_email_attributes\":[\"CONSUMER\",\"_TRUSTED_PROB\"],\"account_email_domain\":\"gmail.com\",\"account_email_first_seen\":\"2021-12-13\",\"account_email_last_event\":\"2024-12-25\",\"account_email_last_update\":\"2024-12-25\",\"account_email_result\":\"success\",\"account_email_score\":\"0\",\"account_email_worst_score\":\"0\",\"account_first_name\":\"ryan\",\"account_last_name\":\"krall\",\"account_login\":\"kralldad40@gmail.com\",\"account_login_activities\":[\"_TRUSTED_PROB\"],\"account_login_attributes\":[\"_TRUSTED_PROB\"],\"account_login_first_seen\":\"2021-12-13\",\"account_login_last_event\":\"2024-12-25\",\"account_login_last_update\":\"2024-12-25\",\"account_login_result\":\"success\",\"account_login_score\":\"0\",\"account_login_worst_score\":\"0\",\"account_name\":\"ryan_krall\",\"account_name_activities\":[\"_TRUSTED_PROB\"],\"account_name_attributes\":[\"_TRUSTED_PROB\"],\"account_name_first_seen\":\"2020-09-07\",\"account_name_last_event\":\"2024-12-25\",\"account_name_last_update\":\"2024-12-25\",\"account_name_result\":\"success\",\"account_name_score\":\"0\",\"account_name_worst_score\":\"0\",\"account_telephone\":\"5132132068\",\"account_telephone_activities\":[\"_TRUSTED_PROB\"],\"account_telephone_attributes\":[\"_TRUSTED_PROB\"],\"account_telephone_country_code\":\"1\",\"account_telephone_first_seen\":\"2021-12-22\",\"account_telephone_geo\":\"US\",\"account_telephone_is_possible\":\"yes\",\"account_telephone_is_valid\":\"yes\",\"account_telephone_last_event\":\"2024-12-25\",\"account_telephone_last_update\":\"2024-12-25\",\"account_telephone_result\":\"success\",\"account_telephone_score\":\"0\",\"account_telephone_type\":\"FIXED_LINE_OR_MOBILE\",\"account_telephone_worst_score\":\"0\",\"agent_model\":\"SM-A037U\",\"agent_publickey_hash\":\"24d6490c138b415a857c724ddbbd9f06c51a6c44\",\"agent_publickey_hash_result\":\"not found\",\"agent_publickey_hash_type\":\"web:ecdsa\",\"agent_type\":\"browser_mobile\",\"api_call_datetime\":\"2024-12-25 21:20:45.573\",\"api_caller_ip\":\"52.89.158.201\",\"api_caller_ip_geo\":\"US\",\"api_type\":\"session-query\",\"api_version\":\"15.5.2\",\"audio_context\":\"d250471a0c0de7f0578c965047b8a94f9f10804194ac35744729bb365910d8d4\",\"battery_status\":\"{\\\"level\\\":0.96,\\\"status\\\":\\\"unplugged\\\"}\",\"bb_assessment\":\"523.41\",\"bb_assessment_rating\":\"neutral\",\"bb_bot_rating\":\"low\",\"bb_bot_score\":\"500.00\",\"bb_fraud_rating\":\"neutral\",\"bb_fraud_score\":\"500.00\",\"behaviosec.confidence\":\"0.0000\",\"behaviosec.event_weight\":\"0.00\",\"behaviosec.pointing_device\":[\"touchscreen\"],\"behaviosec.population_profile.raw_features\":{\"accRes\":0,\"keyRes\":0,\"tAction\":6581,\"actRes\":975.17,\"mouseRes\":340.25,\"winFoc\":1,\"targets\":{\"checkbox#tribalOrdinance\":{\"foc2b\":1029},\"IFRAME#a-mbyvg7s3tdgr\":{\"foc2b\":64},\"checkbox#termsAgree\":{\"foc2b\":820}},\"tActive\":8539},\"behaviosec.recognition_ratio\":\"0.0000\",\"behaviosec.score\":\"500.0000\",\"behaviosec.userid\":\"kralldad40@gmail.com\",\"browser\":\"Chrome\",\"browser_language\":\"en-US,en;q=0.9\",\"browser_string\":\"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36\",\"browser_string_hash\":\"7e91326b17b5455769214abbd4472bb7\",\"browser_version\":\"131\",\"canvas_hash\":\"4ba987ec9909dc9f6f1aea8ce0057ba83dca6741\",\"challenger_policy\":\"FraudScoreCardModel\",\"challenger_policy_score\":\"-65\",\"challenger_reason_code\":[\"HabemcoFraudScore_65\",\"HabemcoFraudScore_64\",\"HabemcoFraudScore_63\",\"HabemcoFraudScore_62\",\"HabemcoFraudScore_61\",\"HabemcoFraudScore_60\",\"HabemcoFraudScore_59\",\"HabemcoFraudScore_58\",\"HabemcoFraudScore_57\",\"HabemcoFraudScore_56\",\"HabemcoFraudScore_55\",\"HabemcoFraudScore_54\",\"HabemcoFraudScore_53\",\"HabemcoFraudScore_52\",\"HabemcoFraudScore_51\",\"HabemcoFraudScore_50\",\"HabemcoFraudScore_45\",\"HabemcoFraudScore_40\",\"HabemcoFraudScore_35\",\"HabemcoFraudScore_30\",\"HabemcoFraudScore_25\",\"HabemcoFraudScore_20\",\"HabemcoFraudScore_15\",\"HabemcoFraudScore_10\",\"HabemcoFraudScore_05\"],\"challenger_request_duration\":\"0\",\"challenger_review_status\":\"pass\",\"challenger_risk_rating\":\"neutral\",\"champion_request_duration\":\"9\",\"cpu_clock\":\"10000\",\"cpu_cores\":\"8\",\"css_image_loaded\":\"yes\",\"device_activities\":[\"_TRUSTED_PROB\"],\"device_attributes\":[\"_TRUSTED_PROB\"],\"device_first_seen\":\"2022-11-28\",\"device_id\":\"1ee122774f644efe8200db8bd181dd5e\",\"device_id_confidence\":\"100\",\"device_last_event\":\"2024-12-25\",\"device_last_update\":\"2024-12-25\",\"device_match_result\":\"success\",\"device_memory\":\"2\",\"device_result\":\"success\",\"device_score\":\"0\",\"device_worst_score\":\"0\",\"digital_id\":\"1bfb398ba6f945539d01c5b05c86e26f\",\"digital_id_activities\":[\"_TRUSTED_PROB\"],\"digital_id_attributes\":[\"_TRUSTED_PROB\"],\"digital_id_confidence\":\"7554\",\"digital_id_confidence_rating\":\"high\",\"digital_id_first_seen\":\"2021-12-13\",\"digital_id_last_event\":\"2024-12-25\",\"digital_id_last_update\":\"2024-12-25\",\"digital_id_result\":\"success\",\"digital_id_trust_score\":\"93.42\",\"digital_id_trust_score_rating\":\"very_high\",\"digital_id_trust_score_reason_code\":[\"_ts_neutral_conf_score\",\"_ts_high_entity_age_score\",\"_ts_very_high_did_age_score\",\"_ts_neutral_did_distance\",\"_ts_neutral_graph_complex\",\"_ts_did_link_new_inputip\",\"_ts_did_2_or_more_phone\",\"_ts_did_3_or_more_trueip\",\"_ts_did_2_or_more_inputip\",\"_ts_did_edge_detect_inputip\",\"_ts_logodds_sum_lua\"],\"digital_id_trust_score_summary_reason_code\":[\"_ts_did_link_new_entity\",\"_ts_did_edge_detect_entity\"],\"dns_ip\":\"65.24.5.242\",\"dns_ip_assert_history\":[\"NEGATIVE_HISTORY\"],\"dns_ip_attributes\":[\"_CHALLENGED\",\"_CHALLENGE_FAILED\",\"_CHALLENGE_PASSED\",\"_TRUSTED_PROB\"],\"dns_ip_city\":\"lexington-fayette urban county\",\"dns_ip_connection_type\":\"tx\",\"dns_ip_geo\":\"US\",\"dns_ip_home\":\"no\",\"dns_ip_isp\":\"charter communications inc\",\"dns_ip_latitude\":\"38.01602\",\"dns_ip_line_speed\":\"high\",\"dns_ip_longitude\":\"-84.48788\",\"dns_ip_organization\":\"charter communications inc\",\"dns_ip_postal_code\":\"40502\",\"dns_ip_region\":\"kentucky\",\"dns_ip_region_iso_code\":\"ky\",\"dns_ip_routing_type\":\"fixed\",\"enabled_ck\":\"yes\",\"enabled_fl\":\"no\",\"enabled_im\":\"yes\",\"enabled_js\":\"yes\",\"enabled_services\":[\"TMX100\",\"TMX300\"],\"enhanced_headers_name_value_hash\":\"85362c512608d485f5045bc52aba8261\",\"etag_guid\":\"98e12c4683fd450da99410d2a82db17b\",\"event_datetime\":\"2024-12-25 21:20:45.573\",\"event_type\":\"account_creation\",\"fuzzy_device_activities\":[\"_TRUSTED_PROB\"],\"fuzzy_device_attributes\":[\"_TRUSTED_PROB\"],\"fuzzy_device_first_seen\":\"2022-11-28\",\"fuzzy_device_id\":\"1ee122774f644efe8200db8bd181dd5e\",\"fuzzy_device_id_confidence\":\"100.00\",\"fuzzy_device_last_event\":\"2024-12-25\",\"fuzzy_device_last_update\":\"2024-12-25\",\"fuzzy_device_match_result\":\"success\",\"fuzzy_device_result\":\"success\",\"fuzzy_device_score\":\"0\",\"fuzzy_device_worst_score\":\"0\",\"global_third_party_cookie\":\"762bea34074d470a934082326c6b93be\",\"gpu_name\":\"PowerVR Rogue GE8320\",\"gpu_vendor\":\"Imagination Technologies\",\"headers_name_value_hash\":\"578a3d46f2e84d7b8ab960b68dbb1304\",\"headers_order_string_hash\":\"244ef9b7ac608f00c8c1f6e495bb2eab\",\"honeypot_fingerprint\":\"54930c2d59e3e56e715547df911c540222045fc8\",\"honeypot_fingerprint_check\":\"true\",\"honeypot_fingerprint_match\":\"match_exact\",\"http_connection_type\":\"Ethernet or modem\",\"http_os_sig_adv_mss\":\"1448\",\"http_os_sig_rcv_mss\":\"1448\",\"http_os_sig_snd_mss\":\"1448\",\"http_os_signature\":\"Linux 2.2.x-3.x\",\"http_referer\":\"j0oGon0BJEkDaA!2389nhcbACHIbnLM7\",\"http_referer_domain\":\"www.uprova.com\",\"http_referer_url\":\"https://www.uprova.com/portal/Apply/Account\",\"image_loaded\":\"yes\",\"input_ip_address\":\"2603:6013:ba00:6599:edf0:3466:9597:1e08\",\"input_ip_city\":\"lexington-fayette urban county\",\"input_ip_connection_type\":\"cable\",\"input_ip_geo\":\"US\",\"input_ip_home\":\"no\",\"input_ip_isp\":\"charter communications inc\",\"input_ip_latitude\":\"38.01602\",\"input_ip_longitude\":\"-84.48788\",\"input_ip_organization\":\"charter communications inc\",\"input_ip_postal_code\":\"40502\",\"input_ip_region\":\"kentucky\",\"input_ip_result\":\"not found\",\"js_browser\":\"Chrome 131\",\"js_browser_string\":\"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36\",\"js_browser_string_hash\":\"7e91326b17b5455769214abbd4472bb7\",\"js_fonts_hash\":\"464002236081462cad6d225433d38dec\",\"js_fonts_number\":\"13\",\"js_os\":\"Android 13\",\"math_routine\":\"4003d1c2bec02e6cc560082ad155401fd4588141d6eaa24dc94afbd72313196a\",\"national_id_activities\":[\"_TRUSTED_PROB\"],\"national_id_attributes\":[\"_TRUSTED_PROB\"],\"national_id_first_seen\":\"2023-10-20\",\"national_id_last_event\":\"2024-12-25\",\"national_id_last_update\":\"2024-12-25\",\"national_id_number\":\"d32956b2948d8198b821be47b79565582788f3545712e30acc4db7c7dae0bf9f\",\"national_id_result\":\"success\",\"national_id_score\":\"0\",\"national_id_type\":\"us_ssn_hash\",\"national_id_worst_score\":\"0\",\"nonce\":\"115485b78a134867\",\"org_id\":\"2lrwrk15\",\"os\":\"Android\",\"os_version\":\"13.0.0\",\"page_elements\":{\"ver\":3,\"redirectUrl\":[false,\"hidden\",4],\"IdNumber\":[false,\"hidden\",4],\"BackValue\":[false,\"hidden\",4],\"tribalOrdinance\":[true,\"checkbox\",4],\"termsAgree\":[true,\"checkbox\",4],\"shareAppInfo\":[true,\"checkbox\",2],\"TenantId\":[false,\"hidden\",4],\"IsExistingCustomer\":[false,\"hidden\",4],\"IsBack\":[false,\"hidden\",4],\"MaxPageReached\":[false,\"hidden\",4],\"ValidPassword\":[false,\"hidden\",4],\"UpdateMaxPageReached\":[false,\"hidden\",4],\"gclid\":[false,\"hidden\",4],\"UTMCampaign\":[false,\"hidden\",4],\"UTMMedium\":[false,\"hidden\",4],\"UTMSource\":[false,\"hidden\",4],\"UTMTerm\":[false,\"hidden\",4],\"irclickid\":[false,\"hidden\",4],\"TMetrixSessionID\":[false,\"hidden\",4],\"TMetrixPageID\":[false,\"hidden\",4],\"__RequestVerificationToken\":[false,\"hidden\",4],\"g-recaptcha-response\":[false,\"textarea\",4]},\"page_time_on\":\"8554\",\"policy\":\"account_registration\",\"policy_details_api\":{\"policy_detail_api\":[{\"customer\":{\"pvid\":\"1967294\",\"review_status\":\"pass\",\"risk_rating\":\"neutral\",\"score\":\"0\"},\"id\":\"0\",\"type\":\"champion\"},{\"customer\":{\"pvid\":\"1973063\",\"review_status\":\"pass\",\"risk_rating\":\"neutral\",\"rules\":[{\"reason_code\":\"HabemcoFraudScore_65\",\"rid\":\"470321470\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_64\",\"rid\":\"470321471\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_63\",\"rid\":\"470321472\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_62\",\"rid\":\"470321473\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_61\",\"rid\":\"470321474\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_60\",\"rid\":\"470321475\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_59\",\"rid\":\"470321476\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_58\",\"rid\":\"470321477\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_57\",\"rid\":\"470321478\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_56\",\"rid\":\"470321479\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_55\",\"rid\":\"470321480\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_54\",\"rid\":\"470321481\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_53\",\"rid\":\"470321482\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_52\",\"rid\":\"470321483\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_51\",\"rid\":\"470321484\",\"score\":\"-1\"},{\"reason_code\":\"HabemcoFraudScore_50\",\"rid\":\"470321485\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_45\",\"rid\":\"470321486\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_40\",\"rid\":\"470321487\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_35\",\"rid\":\"470321488\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_30\",\"rid\":\"470321489\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_25\",\"rid\":\"470321490\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_20\",\"rid\":\"470321491\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_15\",\"rid\":\"470321492\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_10\",\"rid\":\"470321493\",\"score\":\"-5\"},{\"reason_code\":\"HabemcoFraudScore_05\",\"rid\":\"470321494\",\"score\":\"-5\"}],\"score\":\"-65\",\"vars\":[{\"rid\":\"470321418\",\"value\":\"1595841\",\"variable\":\"mins_since_digital_id_first_seen\"},{\"rid\":\"470321429\",\"value\":\"1\",\"variable\":\"smart_id_velocity_local\"}]},\"id\":\"1\",\"type\":\"challenger\"}]},\"policy_engine_version\":\"15.5.2\",\"policy_score\":\"0\",\"primary_industry\":\"banking\",\"private_browsing\":\"no\",\"profile_api_timedelta\":\"13215\",\"profile_connection_type\":\"TLS1.3\",\"profiled_domain\":\"www.uprova.com\",\"profiled_url\":\"https://www.uprova.com/portal/Apply/PromoCode\",\"profiling_datetime\":\"1735161632\",\"profiling_delta\":\"6368\",\"proxy_reason\":\"ML model result\",\"proxy_score\":\"1.64\",\"request_duration\":\"36\",\"request_id\":\"9c592c63-c904-45a2-bc66-2ca6f9a20b0a\",\"request_result\":\"success\",\"review_status\":\"pass\",\"risk_rating\":\"neutral\",\"screen_color_depth\":\"24\",\"screen_res\":\"1600x720\",\"secondary_industry\":\"credit_union\",\"service_type\":\"all\",\"session_id\":\"lnx-43782bfc-3efa-46b1-9e76-e4541ee688b6\",\"session_id_query_count\":\"1\",\"ssl_fp_signature\":\"368244b01a18432c655ebd3657e65480\",\"ssn_hash\":\"d32956b2948d8198b821be47b79565582788f3545712e30acc4db7c7dae0bf9f\",\"ssn_hash_activities\":[\"_TRUSTED_PROB\"],\"ssn_hash_attributes\":[\"_TRUSTED_PROB\"],\"ssn_hash_first_seen\":\"2023-09-01\",\"ssn_hash_last_event\":\"2024-12-25\",\"ssn_hash_last_update\":\"2024-12-25\",\"ssn_hash_result\":\"success\",\"ssn_hash_score\":\"0\",\"ssn_hash_worst_score\":\"0\",\"summary_risk_score\":\"0\",\"system_state\":\"1731248449\",\"third_party_cookie\":\"b39b121f74ec5cc196aa071dfc8c0a83\",\"time_zone\":\"-300\",\"time_zone_dst_offset\":\"60\",\"timezone_name\":\"America/New_York\",\"tmx_reason_code\":[\"_Threat_Intelligence_Real_Location_Detected\",\"_IP_GBL_VEL_1_2_13_96\",\"_IP_GBL_AGE_GT_3MTHS\",\"_IP_GBL_VEL_120_120_120_120\",\"_IP_GBL_AGE_GT_3MTHS\",\"_IP_GBL_AGE_LT_HR\",\"_EXACT_ID_GBL_VEL_1_2_30_120\",\"_EXACT_ID_GBL_AGE_GT_3MTHS\",\"_SMART_ID_GBL_VEL_1_2_30_120\",\"_SMART_ID_GBL_AGE_GT_3MTHS\",\"_ACC_EMAIL_GBL_AGE_GT_3MTHS\",\"_ACC_LOGIN_GBL_AGE_GT_3MTHS\",\"_ACC_TEL_GBL_AGE_GT_3MTHS\",\"_ACC_NAME_GBL_VEL_1_1_1_6\",\"_ACC_NAME_GBL_AGE_GT_3MTHS\",\"_SSN_GBL_AGE_GT_3MTHS\",\"_AGENT_PUBLICKEY_HASH_GBL_AGE_LT_HR\",\"_DIG_ID_GBL_AGE_GT_3MTHS\",\"_national_id_GBL_AGE_GT_3MTHS\",\"_IP_LCL_AGE_GT_3MTHS\",\"_IP_LCL_VEL_1_2_9_44\",\"_IP_LCL_AGE_GT_3MTHS\",\"_IP_LCL_AGE_LT_HR\",\"_EXACT_ID_LCL_AGE_GT_3MTHS\",\"_SMART_ID_LCL_AGE_GT_3MTHS\",\"_ACC_EMAIL_LCL_AGE_GT_3MTHS\",\"_ACC_LOGIN_LCL_AGE_GT_3MTHS\",\"_ACC_TEL_LCL_AGE_GT_3MTHS\",\"_ACC_NAME_LCL_AGE_GT_3MTHS\",\"_SSN_LCL_AGE_GT_3MTHS\",\"_AGENT_PUBLICKEY_HASH_LCL_AGE_LT_HR\",\"_DIG_ID_LCL_AGE_GT_3MTHS\",\"_national_id_LCL_AGE_GT_3MTHS\",\"_ACC_EMAIL_EXACT_ID_GBL_PAIR_AGE_GT_3MTHS\",\"_ACC_EMAIL_SMART_ID_GBL_PAIR_AGE_GT_3MTHS\",\"_ACC_LOGIN_EXACT_ID_GBL_PAIR_AGE_GT_3MTHS\",\"_ACC_LOGIN_SMART_ID_GBL_PAIR_AGE_GT_3MTHS\",\"_ACC_NAME_EXACT_ID_GBL_PAIR_AGE_GT_3MTHS\",\"_ACC_NAME_SMART_ID_GBL_PAIR_AGE_GT_3MTHS\",\"_ACC_EMAIL_EXACT_ID_LCL_PAIR_AGE_GT_3MTHS\",\"_ACC_EMAIL_SMART_ID_LCL_PAIR_AGE_GT_3MTHS\",\"_ACC_LOGIN_EXACT_ID_LCL_PAIR_AGE_GT_3MTHS\",\"_ACC_LOGIN_SMART_ID_LCL_PAIR_AGE_GT_3MTHS\",\"_ACC_NAME_EXACT_ID_LCL_PAIR_AGE_GT_3MTHS\",\"_ACC_NAME_SMART_ID_LCL_PAIR_AGE_GT_3MTHS\",\"_EXPRESSION_ERROR\",\"PID_LCL_AGE_ACC_EMAIL_GT_3MONTHS\",\"_TrueIP_Org_Global_Whitelist\",\"_TrueIP_ISP_Global_Whitelist\",\"_InputIP_Org_Global_Whitelist\",\"_InputIP_ISP_Global_Whitelist\",\"_Email_FRS_accept_global_1_3\",\"_Phone_FRS_accept_global_1_3\",\"_DID_FRS_accept_global_1_3\",\"_Email_FRS_accept_local_1_3\",\"_SmartID_FRS_accept_local_1_3\",\"_Phone_FRS_accept_local_1_3\",\"_DID_FRS_accept_local_1_3\",\"_ExactID_FRS_accept_local_1_3\",\"_TMX_GBL_TT_LIMIT_REACHED_DNS_IP\"],\"tmx_risk_rating\":\"neutral\",\"tmx_summary_reason_code\":[\"Level_1_Link_Accept\"],\"tmx_variables\":{\"_accemail_gbl_velocity_hour\":\"0\",\"_accemail_local_velocity_hour\":\"0\",\"_acclogin_local_velocity_hour\":\"0\",\"_accphone_gbl_velocity_hour\":\"0\",\"_acctemails_per_exactid_hour\":\"0\",\"_acnt_login_per_exactid_hour\":\"0\",\"_acnt_login_per_smartid_hour\":\"0\",\"_acntemails_per_exactid_gbl_hour\":\"0\",\"_acntemails_per_smartid_hour\":\"0\",\"_cc_per_exactid_hour\":\"0\",\"_cc_per_smartid_gbl_hour\":\"0\",\"_cc_per_smartid_hour\":\"0\",\"_exactid_gbl_velocity_hour\":\"0\",\"_exactid_local_velocity_hour\":\"0\",\"_exactid_per_email_gbl_hour\":\"0\",\"_smartid_gbl_velocity_hour\":\"0\",\"_smartid_local_velocity_hour\":\"0\"},\"true_ip\":\"74.132.41.252\",\"true_ip_attributes\":[\"_TRUSTED_CONF\"],\"true_ip_city\":\"hamilton\",\"true_ip_city_confidence\":\"95\",\"true_ip_connection_type\":\"cable\",\"true_ip_country_confidence\":\"99\",\"true_ip_first_seen\":\"2024-04-24\",\"true_ip_geo\":\"US\",\"true_ip_home\":\"no\",\"true_ip_isp\":\"charter communications inc\",\"true_ip_last_event\":\"2024-12-25\",\"true_ip_last_update\":\"2024-12-25\",\"true_ip_latitude\":\"39.39959\",\"true_ip_line_speed\":\"medium\",\"true_ip_longitude\":\"-84.56115\",\"true_ip_organization\":\"charter communications inc\",\"true_ip_organization_type\":\"Internet Service Provider\",\"true_ip_postal_code\":\"45015\",\"true_ip_region\":\"ohio\",\"true_ip_region_iso_code\":\"oh\",\"true_ip_result\":\"success\",\"true_ip_routing_type\":\"fixed\",\"true_ip_score\":\"0\",\"true_ip_source_port\":\"38281\",\"true_ip_state_confidence\":\"97\",\"true_ip_worst_score\":\"0\",\"true_ipv6\":\"2603:6013:ba00:6599:edf0:3466:9597:1e08\",\"true_ipv6_source_port\":\"55716\",\"turn_os_sig_raw\":\"74.132.41.252~4:53:0:1460:65535,10:mss,sok,ts,nop,ws:df,id+:0|4:53:0:0:64,0:nop,nop,ts:df,id+,ts2+:0\",\"turn_os_sig_ttl\":\"53\",\"turn_os_signature\":\"Linux 2.2.x-3.x\",\"ua_browser\":\"chrome 131.0\",\"ua_browser_alt\":\"Chrome Mobile\",\"ua_browser_version_alt\":\"131.0.0.0\",\"ua_mobile\":\"yes\",\"ua_os\":\"android\",\"ua_os_alt\":\"Android\",\"ua_os_version_alt\":\"10\",\"ua_platform\":\"chrome 131.0 for android\",\"vm_index\":\"300\",\"vm_rating\":\"neutral\",\"vm_reason\":\"device_memory\",\"webgl_hash\":\"6b7640628817f65f677a7706b5bc7c077038a3d4\",\"webrtc_external_ip\":\"74.132.41.252\",\"webrtc_internal_ip\":\"192.168.1.9\",\"webrtc_ipv6\":\"2603:6013:ba00:6599:dc66:ccff:fe48:5cf7\",\"webrtc_media\":\"(1,1,1,e12ead95566221e7c4e320e56363dcb477a3c93a4a8510beb25f01bca07d4e8d)\",\"webrtc_media_mini\":\"111e12ead9556622\"}""", + "DeviceId": "1ee122774f644efe8200db8bd181dd5e", + "FuzzyDeviceId": "1ee122774f644efe8200db8bd181dd5e", + "ownhome": None, + "employmentstatus": None, + "lengthatjob": None, + "payfrequency": None, + "lengthatbank": None, + "educationlevel": None, + "monthsatresidence": None + } + + +class TestBlock(unittest.TestCase): + def test_main_success(self): + blockResult = __main__(**input_json) + # breakpoint() + self.assertIsInstance(blockResult, dict, "Result should be a dictionary.") + self.assertIn("hd_score_m1", blockResult, "Result dictionary should contain 'hd_score_m1' if success.") + + + +if __name__ == "__main__": + unittest.main() + + diff --git a/xgboost_model.joblib b/xgboost_model.joblib new file mode 100644 index 0000000..3e9f35e Binary files /dev/null and b/xgboost_model.joblib differ