# Phase G — TTL Counter Management UI + Runtime Writer

**Branch**: `feat/fraud-ttl-counters`
**Parent**: `feat/fraud-rules-jube-parity`
**Jube reference**: `EntityAnalysisModelTtlCounter` POCO + Image-2 "TTL Counter" tab.

---

## 1. Goal

Two halves of one feature:

1. **Admin UI** — operators declare TTL counters (e.g. `Distinct1HourTransactionsForCard`).
2. **Runtime writer** — when an Activation rule fires with `enable_ttl_counter = true`,
   the `SideEffect{:ttl_counter}` already emitted by Phase E is consumed and the
   counter is incremented in Redis (`infra_cache`), with the TTL the schema declares.

The counter values then become available to AbstractionEngine via the existing
`function_type: ttl_counter` aggregation (already wired but currently no producer).

---

## 2. Source-of-truth field mapping

Schema already complete at [`risk_ttl_counter.ex`](../../../apps/infra_repo/lib/infra_repo/schemas/risk_ttl_counter.ex)
(16 fields). Jube extras we skip: `Guid`, `ImportId`, `Deleted*`, `CreatedUser`.

---

## 3. Work breakdown

### 3.1 Migration

- `add :version, :integer, default: 1, null: false`
- `add :created_by, :string`
- Compound index `(tenant_id, entity_model_id, name)` (unique on `name`).

### 3.2 Schema

Add `version`, `created_by`. No other validation changes — schema is mature.

### 3.3 Context

In `InfraRepo.Risk.Risks`:
- `list_ttl_counters(entity_model_id, opts \\ [])`
- `get_ttl_counter/1`, `get_ttl_counter_by_key/2`
- `upsert_ttl_counter/1` (bumps version)
- `delete_ttl_counter/1`

### 3.4 Runtime writer (NEW)

Create `apps/mw_risk/lib/mw_risk/ttl_counter_writer.ex`:

```elixir
defmodule MwRisk.TtlCounterWriter do
  @moduledoc "Consumes {:side_effect, %{kind: :ttl_counter, ...}} on PubSub
  and increments the declared counter in InfraCache (Redis)."
  use GenServer
  alias Phoenix.PubSub
  # subscribes to "risk:side_effect:*" via per-tenant topic registry
end
```

Storage scheme in Redis:
```
ttl:{tenant_id}:{counter_name}:{entity_value}:{resolution_bucket} → count
TTL = counter.ttl_value × counter.ttl_interval
```

Bucket = `truncate(scored_at, counter.resolution_interval)` so multiple events
in the same minute share a slot. AbstractionEngine reads
`SUM(ttl:tenant:counter:entity:*)` over the TTL window.

Add the writer to `MwRisk.Application` supervision tree.

### 3.5 LiveView

`apps/gateway_web/lib/gateway_web_web/live/admin/fraud/ttl_counters_live.ex`
at `/admin/fraud/ttl-counters`. Drawer sections:

1. **Properties** — name, data_name (entity to count), active, locked.
2. **Counting** — `sum_enabled` (toggle between count vs sum), `data_value` (field to sum if enabled), `online_aggregation`, `live_forever`.
3. **TTL** — `ttl_interval` (seconds…years), `ttl_value`, `resolution_interval`.
4. **Output** — `report_table`, `response_payload`.

### 3.6 ActivationRule drawer wiring

In `ActivationRulesLive` drawer's "TTL Counter Increment" section, change the
`ttl_counter_key` text field into a `<select>` populated from
`Risks.list_ttl_counters(entity_model_id)` keyed by name. Same for the
RuleBuilder when authoring a rule that references a counter value.

### 3.7 Sidebar

Add `TTL Counters` → `/admin/fraud/ttl-counters` under the Fraud section.

### 3.8 Tests

- `risk_ttl_counter_test.exs` — interval validation, version bump.
- `ttl_counter_writer_test.exs` — pubsub broadcast → Redis increment with correct TTL; bucket alignment.
- `ttl_counters_live_test.exs` — CRUD + drawer toggle interactions.

### 3.9 Seeds

3 demo counters:
| Name | Data | TTL | Resolution |
|---|---|---|---|
| distinct_txn_1h_per_card | card_pan | 1 hour | minutes |
| sum_amount_1d_per_ip     | ip_address (sum amount) | 1 day | hours |
| txn_count_30d_per_customer | customer_id | 30 days | days |

---

## 4. Acceptance criteria

- [ ] CRUD UI works.
- [ ] An Activation rule firing with `enable_ttl_counter = true` produces a Redis key with the configured TTL.
- [ ] `redis-cli TTL ttl:1:distinct_txn_1h_per_card:<pan>:<bucket>` returns ≈ 3600.
- [ ] AbstractionEngine `ttl_counter` function returns the bucketed sum.
- [ ] ActivationRule drawer's TTL counter dropdown is populated from declared counters.

---

## 5. Out of scope

- HyperLogLog distinct-counting (use Redis SET + EXPIRE in v1).
- Online sliding-window resolution finer than 1 minute.
- Counter snapshot / replay tools.
- Counter visualisation widget on the admin dashboard.
