# Phase 1 — API Gateway + Auth + Banking Adapter

**Duration:** Weeks 3–5
**Status:** ✅ Complete — 2026-04-26
**Goal:** First complete end-to-end request path: REST client → core banking → response + audit.

### Implementation Notes

- `mw_auth` — JWT (Joken HS256), API key (SHA-256 prefix cache), RBAC, MwAuth.Plug
- `mw_transform` — `to_canonical/2` + `from_canonical/1` for payment/balance message types
- `mw_audit` — fire-and-forget async audit via Task.Supervisor, `audit_events` table
- `mw_router` — Pipeline: RateLimit → RouteTable → CircuitBreaker (fuse) → Dispatcher → Audit
- `adapter_banking` — Finch HTTP client implementing `MwKernel.Adapter` behaviour
- `gateway_api` — Phoenix/Bandit REST: `/api/v1/transactions`, `/api/v1/accounts/:id/balance`, `/health/*`
- 45 tests passing across all umbrella apps

---

## Deliverable

A working REST API where:
1. Client sends `POST /api/v1/transactions` with JWT
2. JWT is verified; RBAC checked
3. Request transforms to canonical `MwKernel.Message`
4. Routed to `adapter_banking`
5. Core banking is called (stub/sandbox endpoint for dev)
6. Response mapped and returned as JSON
7. Audit event written to DB
8. All stages emitting telemetry events

---

## Tasks

### 1. mw_auth — JWT + API Key

**Dependencies to add:** `joken`, `argon2_elixir`, `ex_rated`

```
DB migrations:
  - create table api_keys (id, name, key_prefix, key_hash, tenant_id, roles, active, expires_at)
  - create table token_revocations (jti, revoked_at, expires_at)
```

Implement:
- `MwAuth.JWT` — `Joken.Config` with tenant_id + roles claims
- `MwAuth.ApiKey` — ETS index on key_prefix, Argon2 verify
- `MwAuth.RBAC` — `authorize!/2` raises `MwKernel.Error.unauthorized`
- `MwAuth.Plug` — detect scheme, delegate, set `context.user`
- `MwAuth.TokenStore` — revocation ETS + DB persistence

Unit tests — use `Mox` for DB calls; test each strategy in isolation.

### 2. mw_transform — Inbound / Outbound Mapping

Start with hardcoded mapping for transaction message type.
Schema registry and DB-stored rules come in Phase 5.

```elixir
defmodule MwTransform.Mapper do
  def to_canonical("transaction.payment", params) do
    %MwKernel.Message{
      id: UUID.uuid4(),
      type: "transaction.payment",
      source: :gateway_api,
      payload: %{
        amount: params["amount"],
        currency: params["currency"],
        account_from: params["from_account"],
        account_to: params["to_account"]
      },
      inserted_at: DateTime.utc_now()
    }
  end
end
```

### 3. mw_router — Pipeline + Route Table

**Dependencies to add:** `:fuse`

Initial route table seeded from config (DB-backed version in Phase 4):

```elixir
# config/config.exs
config :mw_router, :default_routes, [
  %{message_type: "transaction.payment", adapter_module: "Elixir.AdapterBanking"},
  %{message_type: "account.balance", adapter_module: "Elixir.AdapterBanking"}
]
```

Implement:
- `MwRouter.Pipeline` — Plug.Builder chain
- `MwRouter.RouteTable` — ETS table, load from config/DB
- `MwRouter.Dispatcher` — resolve adapter module, call `behaviour.send/2`
- `MwRouter.CircuitBreaker` — `:fuse` per adapter, 5 failures in 10s opens for 30s
- `MwRouter.RateLimiter` — `ex_rated`, 100 req/min per API key default

### 4. adapter_banking — Core Banking Client

**Dependencies to add:** (finch already exists)

For dev: configure against a sandbox/stub URL.
ISO 8583 codec stub (hardcoded encode/decode for pilot message types).

```elixir
defmodule AdapterBanking do
  @behaviour MwKernel.Adapter

  @impl true
  def connect(config) do
    # Start Finch pool if not running
    {:ok, %{config: config, pool: config.pool_name}}
  end

  @impl true
  def send(state, %MwKernel.Message{type: "transaction.payment"} = msg) do
    payload = AdapterBanking.Transformer.to_banking(msg)
    case AdapterBanking.Client.post(state, "/transactions", payload) do
      {:ok, response} -> {:ok, AdapterBanking.Transformer.from_banking(response)}
      {:error, reason} -> {:error, reason}
    end
  end

  @impl true
  def health_check(state) do
    case AdapterBanking.Client.get(state, "/health") do
      {:ok, %{status: 200}} -> :ok
      _ -> {:error, :unhealthy}
    end
  end

  @impl true
  def disconnect(_state), do: :ok
end
```

### 5. gateway_api — REST Endpoints

**Dependencies to add:** none (bandit, plug, jason already present)

```elixir
# apps/gateway_api/lib/gateway_api_web/router.ex
scope "/api/v1", GatewayApiWeb do
  pipe_through [:api, :authenticated]

  post "/transactions", TransactionController, :create
  get "/transactions/:id", TransactionController, :show
  get "/accounts/:id/balance", AccountController, :balance
end

scope "/health", GatewayApiWeb do
  pipe_through [:api]

  get "/live", HealthController, :live
  get "/ready", HealthController, :ready
end
```

`TransactionController.create/2`:
1. Parse and validate JSON body
2. Build `MwKernel.Context` with `trace_id`
3. Call `MwRouter.Pipeline.call(context, [])`
4. Render response or error from returned context

### 6. mw_audit — Event Persistence

```
DB migration: create table audit_events (
  id, trace_id, tenant_id, user_id, message_type,
  adapter, status, duration_ms, request_snapshot,
  response_snapshot, error, inserted_at
)
```

`MwAudit.Logger` writes events asynchronously via `Task.Supervisor` — never blocks pipeline.

---

## Integration Test

```elixir
test "POST /api/v1/transactions creates transaction in banking system" do
  # Start adapter_banking with a Bypass (HTTP stub)
  Bypass.expect_once(bypass, "POST", "/transactions", fn conn ->
    Plug.Conn.send_resp(conn, 200, Jason.encode!(%{reference: "CBS-123", status: "approved"}))
  end)

  jwt = MwAuth.JWT.generate_token(%{sub: "user-1", roles: ["operator"]})

  conn = build_conn()
    |> put_req_header("authorization", "Bearer " <> jwt)
    |> post("/api/v1/transactions", %{amount: 100, currency: "USD", ...})

  assert json_response(conn, 200)["status"] == "approved"
  assert InfraRepo.Repo.aggregate(MwAudit.Event, :count) == 1
end
```

---

## Acceptance Criteria

- [ ] `POST /api/v1/transactions` with valid JWT returns 200 from banking stub
- [ ] `POST /api/v1/transactions` with invalid JWT returns 401
- [ ] `POST /api/v1/transactions` with exhausted rate limit returns 429
- [ ] Audit event written to DB for every request (success and failure)
- [ ] Circuit breaker opens after 5 consecutive banking failures
- [ ] Telemetry events emitted for each pipeline stage
- [ ] `GET /health/live` returns 200 when process is up
- [ ] `GET /health/ready` returns 200 only when DB and required adapters are healthy
