# Sanctions Implementation — Session Handoff
**Date:** 2026-07-04  
**Branch:** `rebased/fraud-verify-flow-alignment`  
**Scope:** Jube-parity sanctions list — auto-update sources, HTTP poller, LiveView UI  

---

## What Was Built (Complete)

### 1. DB Migrations (all applied ✅)
| File | What |
|------|------|
| `apps/infra_repo/priv/repo/migrations/20261013000001_create_entity_model_sanctions.exs` | `risk_entity_model_sanctions` table — links entity models to sanction entries |
| `apps/infra_repo/priv/repo/migrations/20261013000002_create_sanction_entry_sources.exs` | `risk_sanction_entry_sources` table — HTTP source config per tenant |
| `apps/infra_repo/priv/repo/migrations/20261013000003_alter_sanction_entry_sources_jube_parity.exs` | Adds Jube-parity columns: `name_cols` (string), `skip_rows` (int), `reference_col` (int); removes old `name_col`/`skip_header` |

### 2. Schema
- `apps/infra_repo/lib/infra_repo/schemas/risk_sanction_entry_source.ex` — Ecto schema with Jube-parity fields
- `apps/infra_repo/lib/infra_repo/schemas/risk_entity_model_sanction.ex` — join schema

### 3. Context (Risks module)
`apps/infra_repo/lib/infra_repo/risk/risks.ex` — added:
- `list_sanction_sources/1`, `list_active_sanction_sources/0`
- `get_sanction_source/1`, `create_sanction_source/1`, `update_sanction_source/2`, `delete_sanction_source/1`
- `toggle_sanction_source/2`, `mark_source_polled/4`

### 4. GenServer Poller
`apps/mw_risk/lib/mw_risk/sanction_source_poller.ex`
- Polls each active `RiskSanctionEntrySource` on schedule (`poll_interval_hours`)
- Parses CSV using Jube's `MultiPartStringIndex` (multi-column name concat), `Skip` (int header rows), `ReferenceIndex`
- HTTP via `MwRisk.Finch` — **follows up to 5 redirects** (3xx Location header)
- Upserts rows into `risk_sanctions`, reloads ETS cache after each poll
- Public API: `poll_now(source_id)`, `reload_schedule/0`
- Added to supervisor in `apps/mw_risk/lib/mw_risk/application.ex`

### 5. LiveView UI
`apps/gateway_web/lib/gateway_web_web/live/sanctions_live.ex` + `.html.heex`
- Two tabs: **Entries** (existing sanction search) + **Auto-Update Sources**
- Sources tab: list, add, edit, delete, toggle active/inactive, Poll now
- Route: `/admin/fraud/sanctions`

### 6. Model Config Link
`apps/gateway_web/lib/gateway_web_web/live/model_config_live.html.heex`
- "Sanctions" sub-page added to model config grid

### 7. Seed
`apps/infra_repo/priv/repo/seeds/sanction_entry_sources.exs`
- Run with: `cd apps/infra_repo && mix run priv/repo/seeds/sanction_entry_sources.exs`
- **Upserts** (update on re-run, not skip) — safe to run repeatedly

---

## Current Source Configuration (DB state as of this session)

| Source | list_type | URL | delimiter | name_cols | skip_rows | reference_col | entity_type_col | country_code_col | Active |
|--------|-----------|-----|-----------|-----------|-----------|---------------|-----------------|------------------|--------|
| **SDN** | ofac | `https://www.treasury.gov/ofac/downloads/sdn.csv` | `,` | `"1"` | 0 | 0 | 2 | nil | ✅ |
| **BOE** | custom | `https://sanctionslist.fcdo.gov.uk/docs/UK-Sanctions-List.csv` | `,` | `"4"` | 2 | 1 | 44 | 29 | ✅ |
| **EU** | eu | `https://webgate.ec.europa.eu/europeaid/fsd/fsf/public/files/csvFullSanctionsList/content?token=dG9rZW4tMjAxNw` | `;` | `"17"` | 1 | 8 | nil | nil | ❌ (off) |
| **SDN ALT** | ofac | `https://www.treasury.gov/ofac/downloads/alt.csv` | `,` | `"3"` | 0 | 1 | nil | nil | ✅ |

### Column notes
- **SDN**: `col 0` = entry_num (ref), `col 1` = entity name, `col 2` = SDN type (Individual/Entity/Vessel/Aircraft). No header rows. URL redirects 302→302→200 (poller now follows).
- **BOE**: UK Sanctions List (FCDO). Replaced old OFSI Azure blob (withdrawn Jan 2026). `Row 0` = metadata ("Report Date: …"), `Row 1` = header, data from row 2. `col 4` = "Name 6" (full combined name). `col 1` = Unique ID (ref).
- **SDN ALT**: `col 0` = ent_num, `col 1` = alt_num (ref), `col 2` = alt_type, `col 3` = alt_name. Comma-delimited (was incorrectly seeded as `;` — fixed this session).
- **EU**: Disabled. The `?token=dG9rZW4tMjAxNw` ("token-2017") token from Jube's source — needs verification before enabling.

---

## Pending Items

### 1. App restart required ⚠️
The redirect-following fix in `fetch_url/1` was applied this session. The running app has old code in memory. **Restart the Phoenix app** then use **Poll now** on SDN, BOE, SDN ALT to confirm errors clear.

### 2. Quoted CSV fields
The current CSV parser uses naive `String.split(line, delimiter)`. OFAC SDN has names like `"ANGLO-CARIBBEAN CO., LTD."` — the comma inside quotes causes the split to break the name. Impact: entries whose names contain commas get truncated.
- Fix: swap `String.split` for an RFC 4180-aware parser (e.g., `NimbleCSV` or a small hand-rolled state machine)
- File: `apps/mw_risk/lib/mw_risk/sanction_source_poller.ex`, function `parse_body/2`, line ~185

### 3. EU source verification
Before enabling EU: verify `https://webgate.ec.europa.eu/europeaid/fsd/...?token=dG9rZW4tMjAxNw` is still live. The Europa subdomain was `europeaid` in Jube's codebase; it may have moved to `webgate.ec.europa.eu/fsd/` (without `europeaid`).

### 4. Upsert dedup strategy
Current `upsert_rows/2` uses `on_conflict: :nothing` — if an entry already exists (same name+tenant), it's skipped silently. This is fine for initial load but means name changes on an existing entry are never updated. Consider `on_conflict: [set: [last_updated: now]]` if live updates matter.

---

## Key Files (full paths from umbrella root)

```
apps/
  infra_repo/
    lib/infra_repo/
      schemas/
        risk_sanction_entry_source.ex       ← Ecto schema
        risk_entity_model_sanction.ex       ← join schema
      risk/risks.ex                         ← context (source CRUD + mark_polled)
    priv/repo/
      migrations/
        20261013000001_create_entity_model_sanctions.exs
        20261013000002_create_sanction_entry_sources.exs
        20261013000003_alter_sanction_entry_sources_jube_parity.exs
      seeds/
        sanction_entry_sources.exs          ← upsert-safe seed
  mw_risk/
    lib/mw_risk/
      sanction_source_poller.ex             ← HTTP poller GenServer
      sanctions_cache.ex                    ← ETS cache (existing)
      application.ex                        ← SanctionSourcePoller added to supervisor
  gateway_web/
    lib/gateway_web_web/live/
      sanctions_live.ex                     ← LiveView (entries + sources tabs)
      sanctions_live.html.heex
      model_config_live.html.heex           ← "Sanctions" sub-page link added
```

---

## How to Test After Restart

1. Restart the app: `mix phx.server` (or however you start it)
2. Navigate to `/admin/fraud/sanctions` → **Auto-Update Sources** tab
3. Click **Poll now** on SDN — should show `last_count > 0` and no error
4. Click **Poll now** on BOE — same
5. Click **Poll now** on SDN ALT — same
6. Switch to **Entries** tab — confirm entries populated
7. Go to any Entity Model config page → sub-page grid shows "Sanctions" link

---

## Jube Reference
Jube source in repo: `aml-fraud-transaction-monitoring-master/` (at project root)  
Relevant Jube files for sanctions parity:
- `Engine/Sanctions/SanctionEntrySource.cs` — `MultiPartStringIndex`, `Skip`, `ReferenceIndex` field names
- `Engine/Sanctions/SanctionMatcher.cs` — matching algorithm (Levenshtein distance threshold)
- DB migration with `AddSanctionEntrySourceTable` — original 4-source seed (SDN, BOE, EU, SDN ALT)

---

## Jube Field Mapping (for reference)

| Jube field | Our field | Notes |
|-----------|-----------|-------|
| `MultiPartStringIndex` | `name_cols` | Comma-sep column indices, concatenated with space |
| `Skip` | `skip_rows` | Integer, not bool |
| `ReferenceIndex` | `reference_col` | Column used as entry identity for dedup |
| `Delimiter` | `delimiter` | Single char string |
| `EnableHttpDataSource` | `active` | Boolean |
| `HttpDataSourcePollIntervalInSeconds` | `poll_interval_hours` | We use hours; convert on read |
