defmodule DaProductApp.RiskManagement.Seeds do @moduledoc """ Seeds for Risk Management - creates default risk rules for all merchant categories. """ alias DaProductApp.Repo alias DaProductApp.RiskManagement.RiskRule def seed_risk_rules do categories = ["Cat A", "Cat B", "Cat C", "Cat D"] rules_definitions = [ %{ name: "Suspicious International transactions", description: "International card transactions with value ≥ AED 10,000", rule_type: "hold", execution_order: 1, mcc_codes: [], parameters: %{ "threshold" => 10000, "card_types" => ["international"], "currency" => "AED" } }, %{ name: "Multiple Int'l card, low value", description: "2+ international transactions, same terminal, 2,000–5,000 AED within 30 minutes", rule_type: "hold", execution_order: 2, mcc_codes: [], parameters: %{ "min_amount" => 2000, "max_amount" => 5000, "min_count" => 2, "time_window_minutes" => 30, "card_types" => ["international"] } }, %{ name: "Abnormal Time transaction", description: "Transactions between 12AM–7AM, except certain MCCs", rule_type: "hold", execution_order: 3, mcc_codes: [], parameters: %{ "start_hour" => 0, "end_hour" => 7, "excluded_mccs" => ["8062", "7011", "5542", "5812"] } }, %{ name: "Split transaction", description: "Same card+terminal+city, ≤10 minutes, sum ≥ AED 10,000", rule_type: "hold", execution_order: 4, mcc_codes: [], parameters: %{ "time_window_minutes" => 10, "sum_threshold" => 10000, "grouping_fields" => ["card_number", "terminal_id", "location"] } }, %{ name: "Duplicate transaction", description: "Same card+terminal+amount, ≤10 minutes, sum ≥ AED 10,000", rule_type: "hold", execution_order: 5, mcc_codes: [], parameters: %{ "time_window_minutes" => 10, "sum_threshold" => 10000, "grouping_fields" => ["card_number", "terminal_id", "amount"] } }, %{ name: "High Velocity transaction", description: "Same terminal, ≤2 minutes, value ≥ AED 500", rule_type: "hold", execution_order: 6, mcc_codes: [], parameters: %{ "time_window_minutes" => 2, "amount_threshold" => 500, "grouping_fields" => ["terminal_id"] } }, %{ name: "Above-average ticket size", description: "≥4x average ticket size on specific MCCs", rule_type: "hold", execution_order: 7, mcc_codes: ["4812", "5732", "5733", "5541"], parameters: %{ "multiplier" => 4.0, "rolling_days" => 30, "target_mccs" => ["4812", "5732", "5733", "5541"] } }, %{ name: "Dormant Merchant", description: "Transaction after 30 days of inactivity", rule_type: "hold", execution_order: 8, mcc_codes: [], parameters: %{ "inactivity_days" => 30 } }, %{ name: "High Value Transaction", description: "Single transaction value ≥ AED 10,000", rule_type: "hold", execution_order: 9, mcc_codes: [], parameters: %{ "threshold" => 10000, "currency" => "AED" } }, %{ name: "High Frequency Chargeback Merchant", description: "Merchant with 2+ chargebacks in 30 days", rule_type: "hold", execution_order: 10, mcc_codes: [], parameters: %{ "chargeback_count" => 2, "period_days" => 30 } }, %{ name: "Excessive Refunds/Reversals", description: "Refund/reversal rate >10% in 30 days", rule_type: "alert", execution_order: 11, mcc_codes: [], parameters: %{ "refund_rate_threshold" => 0.10, "period_days" => 30 } }, %{ name: "Out-of-Profile Geographic", description: "Unusual card geography pattern", rule_type: "hold", execution_order: 12, mcc_codes: [], parameters: %{ "analysis_window_days" => 30, "deviation_threshold" => 3.0 } }, %{ name: "Round Figure Transaction on same Terminal", description: "High frequency of round figure transactions (multiples of 100) - >25% in last 90 days", rule_type: "hold", execution_order: 13, mcc_codes: [], parameters: %{ "round_figure_threshold_percent" => 0.25, "period_days" => 90, "round_multiples" => [100, 1000] } }, %{ name: "Transaction followed by Fraud-Suspect declined (Same Card)", description: "Transaction followed by fraud-suspect declined transaction with same card", rule_type: "hold", execution_order: 14, mcc_codes: [], parameters: %{ "fraud_response_codes" => ["04", "07", "41", "43", "59", "63", "65", "67", "69"], "time_window_minutes" => 60 } }, %{ name: "Transaction followed by Fraud-Suspect declined (Different Card)", description: "Transaction followed by fraud-suspect declined with different card but same amount", rule_type: "hold", execution_order: 15, mcc_codes: [], parameters: %{ "fraud_response_codes" => ["04", "07", "41", "43", "59", "63", "65", "67", "69"], "time_window_minutes" => 60 } }, %{ name: "Transaction followed by Wrong PIN Entry Attempts", description: "Transaction followed by wrong PIN entry attempts (4+ attempts)", rule_type: "hold", execution_order: 16, mcc_codes: [], parameters: %{ "wrong_pin_codes" => ["55", "75"], "attempt_threshold" => 4, "time_window_minutes" => 60 } }, %{ name: "Daily Volume Above Average (4x)", description: "Daily transaction volume >= 4x average daily volume of last 90 days", rule_type: "hold", execution_order: 17, mcc_codes: [], parameters: %{ "multiplier" => 4.0, "rolling_days" => 90 } }, %{ name: "Multiple Transactions with Same Amount", description: "Multiple transactions (>5) with same amount at same terminal using different cards in 24hrs, sum >50,000 AED", rule_type: "hold", execution_order: 18, mcc_codes: [], parameters: %{ "transaction_count_threshold" => 5, "time_window_hours" => 24, "sum_threshold" => 50_000 } }, %{ name: "Self Card Transactions by Merchant", description: "Merchant using own card on own terminal, sum >2,000 AED in a day", rule_type: "hold", execution_order: 19, mcc_codes: [], parameters: %{ "daily_threshold" => 2_000 } }, %{ name: "Out-of-Profile Geographic Shift (Domestic to International)", description: "Merchant suddenly receives transactions from new international geographies", rule_type: "hold", execution_order: 20, mcc_codes: [], parameters: %{ "analysis_window_days" => 30, "new_geography_threshold" => 0.20 } }, %{ name: "High Chargeback Rate (>1%)", description: "Merchants with chargeback rate >1% in 30-day period", rule_type: "alert", execution_order: 21, mcc_codes: [], parameters: %{ "chargeback_rate_threshold" => 0.01, "period_days" => 30 } }, %{ name: "High Fraud Rate (>0.5%)", description: "Merchants with fraud rate >0.5% of total transactions in a calendar month", rule_type: "alert", execution_order: 22, mcc_codes: [], parameters: %{ "fraud_rate_threshold" => 0.005, "period_days" => 30 } }, %{ name: "Round or Unusual Amount Patterns (Alert)", description: "High proportion (>25%) of round figure transactions in 90-day window", rule_type: "alert", execution_order: 23, mcc_codes: [], parameters: %{ "round_figure_threshold_percent" => 0.25, "period_days" => 90, "round_multiples" => [100, 1000] } }, %{ name: "High Decline/Authorization Failure Rate (>20%)", description: "Merchants where >20% of transaction attempts are declined", rule_type: "alert", execution_order: 24, mcc_codes: [], parameters: %{ "decline_rate_threshold" => 0.20, "period_days" => 30 } }, %{ name: "Sudden Drop in Daily Transaction Volume", description: "Daily volume drops >50% compared to 30-day average for 3 consecutive days", rule_type: "alert", execution_order: 25, mcc_codes: [], parameters: %{ "drop_threshold_percent" => 0.50, "rolling_days" => 30, "consecutive_days" => 3 } } ] # Create rules for each category with category-specific parameters for category <- categories do for rule_def <- rules_definitions do # Adjust parameters based on category adjusted_params = adjust_parameters_for_category(rule_def.parameters, category) attrs = %{ name: rule_def.name, description: rule_def.description, category: category, rule_type: rule_def.rule_type, execution_order: rule_def.execution_order, mcc_codes: rule_def.mcc_codes, parameters: adjusted_params, enabled: true } case Repo.get_by(RiskRule, name: rule_def.name, category: category) do nil -> %RiskRule{} |> RiskRule.changeset(attrs) |> Repo.insert!() existing -> existing |> RiskRule.changeset(attrs) |> Repo.update!() end end end IO.puts("✅ Risk rules seeded successfully for all categories") end # Adjust rule parameters based on merchant category/risk tier defp adjust_parameters_for_category(base_params, category) do case category do "Cat A" -> # Enterprise Merchant - Lower thresholds, stricter rules base_params |> Map.put("threshold", Map.get(base_params, "threshold", 10000) * 0.8) |> Map.put("multiplier", Map.get(base_params, "multiplier", 4.0) * 0.8) "Cat B" -> # Mid-Market Merchant - Standard thresholds base_params "Cat C" -> # Known Entity - Slightly higher thresholds base_params |> Map.put("threshold", Map.get(base_params, "threshold", 10000) * 1.2) |> Map.put("multiplier", Map.get(base_params, "multiplier", 4.0) * 1.2) "Cat D" -> # SME/SMB - Higher thresholds, more lenient base_params |> Map.put("threshold", Map.get(base_params, "threshold", 10000) * 1.5) |> Map.put("multiplier", Map.get(base_params, "multiplier", 4.0) * 1.5) |> Map.put("time_window_minutes", Map.get(base_params, "time_window_minutes", 10) * 2) _ -> base_params end end end