# Outbound Pipeline

The outbound pipeline handles file uploads from your FluxiQ STA system to BCB STA.

## Architecture Overview

```
┌─────────────────┐
│   FluxiQ STA    │
│   Application   │
└────────┬────────┘
         │
         │ POST /api/v1/files
         ▼
┌─────────────────┐
│ Phoenix Router  │
│ (Port 4000)     │
└────────┬────────┘
         │
         │ OutboundController
         ▼
┌─────────────────┐
│  Ecto Changeset │
│  Validation     │
└────────┬────────┘
         │
         │ Task.Supervisor
         ▼
┌─────────────────┐
│  STA Client     │
│  GenServer      │
└────────┬────────┘
         │
         │ SOAP/HTTPS
         ▼
┌─────────────────┐
│   BCB STA       │
│   WebService    │
└─────────────────┘
```

## How It Works

### 1. File Submission

Submit files via the REST API:

```bash
curl -X POST http://localhost:4000/api/v1/files \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "system": "CCS",
    "file_type": "GCCS001",
    "data": {
      "content": "SGVsbG8gV29ybGQ=",
      "file_name": "CCS_20260131_001.txt"
    },
    "destination": {
      "ispbs": ["12345678"]
    }
  }'
```

Response:

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "protocol": "2026013100001234",
  "status": "uploaded",
  "created_at": "2026-01-31T10:30:00Z",
  "file_name": "CCS_20260131_001.txt",
  "file_size": 12345,
  "checksum": "sha256:abc123..."
}
```

### 2. Protocol Request

The connector requests an upload protocol from BCB using the STA Client GenServer:

```elixir
# Request protocol for upload via GenServer
{:ok, protocol_resp} = StaConnector.StaClient.request_protocol(%{
  nome_arquivo: file_name,
  tipo_arquivo: file_type,
  ispbs_destino: ["12345678"]
})
# Returns: %{protocol_number: "2026013100001234"}
```

### 3. File Upload

With the protocol number, the file is uploaded asynchronously:

```elixir
# Upload file content to BCB via Task.Supervisor
Task.Supervisor.async_nolink(StaConnector.TaskSupervisor, fn ->
  StaConnector.StaClient.upload_file(protocol_number, content)
end)
```

### 4. Status Tracking

Your system can poll for upload status or receive real-time updates via Phoenix Channels.

## API Reference

### Submit File

```http
POST /api/v1/files
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "system": "CCS",
  "file_type": "GCCS001",
  "data": {
    "content": "base64-encoded-content",
    "file_name": "optional_filename.txt"
  },
  "destination": {
    "ispbs": ["12345678", "87654321"]
  },
  "metadata": {
    "reference": "your-internal-id"
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `system` | string | Yes | BCB system code (CCS, SPI, etc.) |
| `file_type` | string | Yes | BCB file type identifier |
| `data.content` | string | Yes | Base64-encoded file content |
| `data.file_name` | string | No | Custom file name (auto-generated if omitted) |
| `destination.ispbs` | array | No | Destination ISPB codes |
| `metadata` | object | No | Custom metadata for tracking |

### Get File Status

```http
GET /api/v1/files/{id}/status
Authorization: Bearer {api_key}
```

Response:

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "system": "CCS",
  "file_name": "CCS_20260131_001.txt",
  "file_type": "GCCS001",
  "file_size": 12345,
  "checksum": "sha256:abc123...",
  "status": "completed",
  "protocol": "2026013100001234",
  "sta_status": "PROCESSADO",
  "sta_message": null,
  "created_at": "2026-01-31T10:30:00Z",
  "updated_at": "2026-01-31T10:30:15Z"
}
```

### Health Check

```http
GET /api/v1/health
```

Response:

```json
{
  "status": "healthy",
  "sta_connection": "ok",
  "database": "ok",
  "uptime_seconds": 86400,
  "version": "1.0.0"
}
```

## File States

Files move through the following states:

```
┌─────────┐    ┌────────────┐    ┌───────────┐
│ pending │───▶│ uploading  │───▶│ uploaded  │
└─────────┘    └─────┬──────┘    └─────┬─────┘
                     │                  │
                     ▼                  ▼
               ┌──────────┐      ┌───────────┐
               │  failed  │      │ completed │
               └──────────┘      └───────────┘
```

| Status | Description |
|--------|-------------|
| `pending` | Queued for processing |
| `uploading` | Protocol received, uploading content |
| `uploaded` | Successfully uploaded to BCB |
| `processing` | BCB is processing the file |
| `completed` | BCB accepted the file |
| `failed` | Upload or processing failed |

## Configuration

### Phoenix Endpoint Setup

```elixir
# config/config.exs
config :sta_connector, StaConnectorWeb.Endpoint,
  url: [host: "localhost"],
  http: [port: 4000],
  secret_key_base: System.get_env("SECRET_KEY_BASE")

config :sta_connector, :outbound_api,
  enabled: true,
  auth_type: :api_key,
  api_key: System.get_env("OUTBOUND_API_KEY")
```

### Authentication Types

#### API Key

```elixir
# config/config.exs
config :sta_connector, :outbound_api,
  auth_type: :api_key,
  api_key: System.get_env("OUTBOUND_API_KEY")
```

Usage:

```bash
curl -H "Authorization: Bearer ${API_KEY}" ...
# or
curl -H "X-API-Key: ${API_KEY}" ...
```

#### No Authentication (Development Only)

```elixir
# config/dev.exs
config :sta_connector, :outbound_api,
  auth_type: :none
```

## File Validation

### Ecto Changeset Validation

The connector validates files using Ecto changesets before submission:

```elixir
defmodule StaConnector.Outbound.FileSubmission do
  use Ecto.Schema
  import Ecto.Changeset

  @valid_systems ~w(CCS CIR CMP STR SPI CAM LDL DICT)

  embedded_schema do
    field :system, :string
    field :file_type, :string
    field :content, :string
    field :file_name, :string
    embeds_one :destination, Destination
  end

  def changeset(submission, attrs) do
    submission
    |> cast(attrs, [:system, :file_type, :content, :file_name])
    |> validate_required([:system, :file_type, :content])
    |> validate_inclusion(:system, @valid_systems)
    |> validate_base64(:content)
    |> validate_content_size(:content, max: 50 * 1024 * 1024)
  end
end
```

| Check | Description |
|-------|-------------|
| Content size | Maximum 50 MB |
| Base64 encoding | Must be valid base64 |
| System ID | Must be valid BCB system |
| File type | Must be valid for the system |

### Validation Error

```json
{
  "error": "validation_error",
  "message": "data.content must be valid base64"
}
```

## Error Handling

### Common Errors

| Error Code | HTTP Status | Description | Resolution |
|------------|-------------|-------------|------------|
| `invalid_request` | 400 | Malformed request | Check request body format |
| `validation_error` | 400 | Validation failed | Fix validation errors |
| `sta_auth_error` | 502 | STA authentication failed | Check credentials |
| `sta_rate_limit` | 429 | BCB rate limit exceeded | Wait and retry |
| `sta_error` | 502 | STA service error | Check BCB status |
| `sta_connection_error` | 502 | Cannot connect to STA | Check network |
| `internal_error` | 500 | Unexpected error | Check logs |

### Error Response

```json
{
  "error": "sta_rate_limit",
  "message": "STA rate limit exceeded"
}
```

## Retry Handling

### Automatic Retries

The connector automatically retries on transient failures:

- Network timeouts
- STA temporary errors (5xx)
- Rate limit with Retry-After header

Retry behavior:

| Setting | Value |
|---------|-------|
| Max retries | 3 (configurable) |
| Backoff | Exponential (1s, 2s, 4s) |

### Manual Retry

Retry a failed upload via Admin API:

```bash
curl -u admin:password -X POST \
  http://localhost:4000/admin/retry/{file_id}
```

## Status Tracking

### Polling

Poll for status updates:

```bash
# Check status
curl -H "Authorization: Bearer ${API_KEY}" \
  http://localhost:4000/api/v1/files/{id}/status
```

### Real-time Updates via Phoenix Channels

Subscribe to file status updates in real-time:

```elixir
# JavaScript client
import {Socket} from "phoenix"

let socket = new Socket("ws://localhost:4000/socket", {
  params: {token: window.userToken}
})

socket.connect()

let channel = socket.channel("files:550e8400-e29b-41d4-a716-446655440000", {})

channel.on("status_update", payload => {
  console.log("File status:", payload.status)
})

channel.join()
  .receive("ok", resp => console.log("Joined channel"))
  .receive("error", resp => console.log("Unable to join", resp))
```

### Status Refresh

The connector refreshes status from BCB when you query files in `uploaded` or `processing` status.

## Monitoring

### Metrics

View outbound metrics via Admin API:

```bash
curl -u admin:password http://localhost:4000/admin/metrics
```

```json
{
  "timestamp": "2026-01-31T10:30:00Z",
  "outbound": {
    "files_submitted": 5000,
    "files_uploaded": 4995,
    "files_failed": 3,
    "files_pending": 2
  }
}
```

### Key Metrics

| Metric | Description |
|--------|-------------|
| `files_submitted` | Total files submitted |
| `files_uploaded` | Successfully uploaded to BCB |
| `files_failed` | Failed uploads |
| `files_pending` | Awaiting upload |

## Code Examples

### cURL

```bash
# Submit file
curl -X POST http://localhost:4000/api/v1/files \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "system": "CCS",
    "file_type": "GCCS001",
    "data": {
      "content": "'$(base64 -w0 file.txt)'"
    }
  }'

# Check status
curl http://localhost:4000/api/v1/files/550e8400-.../status \
  -H "Authorization: Bearer your-api-key"
```

### Elixir

```elixir
defmodule MyApp.StaClient do
  @api_url "http://localhost:4000/api/v1"
  @api_key "your-api-key"

  def submit_file(file_path, system, file_type) do
    # Read and encode file
    content = file_path |> File.read!() |> Base.encode64()
    file_name = Path.basename(file_path)

    # Prepare request body
    body = Jason.encode!(%{
      system: system,
      file_type: file_type,
      data: %{
        content: content,
        file_name: file_name
      }
    })

    # Submit file
    headers = [
      {"Authorization", "Bearer #{@api_key}"},
      {"Content-Type", "application/json"}
    ]

    case HTTPoison.post("#{@api_url}/files", body, headers) do
      {:ok, %{status_code: 201, body: resp_body}} ->
        {:ok, Jason.decode!(resp_body)}

      {:ok, %{status_code: status, body: resp_body}} ->
        {:error, {status, Jason.decode!(resp_body)}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def get_status(file_id) do
    headers = [{"Authorization", "Bearer #{@api_key}"}]

    case HTTPoison.get("#{@api_url}/files/#{file_id}/status", headers) do
      {:ok, %{status_code: 200, body: resp_body}} ->
        {:ok, Jason.decode!(resp_body)}

      {:ok, %{status_code: status, body: resp_body}} ->
        {:error, {status, Jason.decode!(resp_body)}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  def poll_until_complete(file_id, max_attempts \\ 30) do
    poll_until_complete(file_id, max_attempts, 0)
  end

  defp poll_until_complete(_file_id, max_attempts, attempts) when attempts >= max_attempts do
    {:error, :timeout}
  end

  defp poll_until_complete(file_id, max_attempts, attempts) do
    case get_status(file_id) do
      {:ok, %{"status" => status}} when status in ["completed", "failed"] ->
        {:ok, status}

      {:ok, %{"status" => _status}} ->
        Process.sleep(2000)
        poll_until_complete(file_id, max_attempts, attempts + 1)

      {:error, reason} ->
        {:error, reason}
    end
  end
end

# Usage
{:ok, result} = MyApp.StaClient.submit_file("file.txt", "CCS", "GCCS001")
IO.puts("Submitted: #{result["id"]}, Protocol: #{result["protocol"]}")

{:ok, final_status} = MyApp.StaClient.poll_until_complete(result["id"])
IO.puts("Final status: #{final_status}")
```

### Phoenix Controller

```elixir
defmodule StaConnectorWeb.OutboundController do
  use StaConnectorWeb, :controller

  alias StaConnector.Outbound
  alias StaConnector.Outbound.FileSubmission

  action_fallback StaConnectorWeb.FallbackController

  def create(conn, params) do
    with {:ok, changeset} <- FileSubmission.changeset(%FileSubmission{}, params),
         {:ok, submission} <- Outbound.submit_file(changeset) do
      conn
      |> put_status(:created)
      |> render(:show, submission: submission)
    end
  end

  def status(conn, %{"id" => id}) do
    with {:ok, file} <- Outbound.get_file(id) do
      render(conn, :status, file: file)
    end
  end

  def health(conn, _params) do
    health = Outbound.health_check()
    render(conn, :health, health: health)
  end
end
```

### STA Client GenServer

```elixir
defmodule StaConnector.StaClient do
  use GenServer

  @sta_endpoint "https://sta-h.bcb.gov.br/staws"

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def request_protocol(params) do
    GenServer.call(__MODULE__, {:request_protocol, params})
  end

  def upload_file(protocol_number, content) do
    GenServer.call(__MODULE__, {:upload_file, protocol_number, content}, :infinity)
  end

  @impl true
  def init(opts) do
    {:ok, %{
      endpoint: opts[:endpoint] || @sta_endpoint,
      credentials: opts[:credentials]
    }}
  end

  @impl true
  def handle_call({:request_protocol, params}, _from, state) do
    result = do_request_protocol(params, state)
    {:reply, result, state}
  end

  @impl true
  def handle_call({:upload_file, protocol_number, content}, _from, state) do
    result = do_upload_file(protocol_number, content, state)
    {:reply, result, state}
  end

  defp do_request_protocol(params, state) do
    # SOAP request to BCB for protocol
    soap_body = build_protocol_request_soap(params)

    case HTTPoison.post(state.endpoint, soap_body, soap_headers(state)) do
      {:ok, %{status_code: 200, body: body}} ->
        parse_protocol_response(body)

      {:ok, %{status_code: status, body: body}} ->
        {:error, {:sta_error, status, body}}

      {:error, reason} ->
        {:error, {:connection_error, reason}}
    end
  end

  defp do_upload_file(protocol_number, content, state) do
    # SOAP request to BCB for file upload
    soap_body = build_upload_soap(protocol_number, content)

    case HTTPoison.post(state.endpoint, soap_body, soap_headers(state)) do
      {:ok, %{status_code: 200, body: body}} ->
        parse_upload_response(body)

      {:ok, %{status_code: status, body: body}} ->
        {:error, {:sta_error, status, body}}

      {:error, reason} ->
        {:error, {:connection_error, reason}}
    end
  end

  defp soap_headers(state) do
    [
      {"Content-Type", "text/xml; charset=utf-8"},
      {"SOAPAction", ""},
      {"Authorization", "Basic #{state.credentials}"}
    ]
  end

  defp build_protocol_request_soap(_params), do: "..."
  defp build_upload_soap(_protocol, _content), do: "..."
  defp parse_protocol_response(_body), do: {:ok, %{}}
  defp parse_upload_response(_body), do: {:ok, %{}}
end
```

### Async File Processing with Task.Supervisor

```elixir
defmodule StaConnector.Outbound.FileProcessor do
  require Logger

  def process_file_async(file_submission) do
    Task.Supervisor.async_nolink(StaConnector.TaskSupervisor, fn ->
      process_file(file_submission)
    end)
  end

  def process_file(file_submission) do
    with {:ok, protocol} <- StaConnector.StaClient.request_protocol(file_submission),
         :ok <- update_status(file_submission.id, :uploading, protocol.protocol_number),
         {:ok, _} <- StaConnector.StaClient.upload_file(protocol.protocol_number, file_submission.content),
         :ok <- update_status(file_submission.id, :uploaded) do
      # Broadcast status update via Phoenix Channel
      StaConnectorWeb.Endpoint.broadcast(
        "files:#{file_submission.id}",
        "status_update",
        %{status: "uploaded", protocol: protocol.protocol_number}
      )

      {:ok, :uploaded}
    else
      {:error, reason} ->
        Logger.error("File processing failed: #{inspect(reason)}")
        update_status(file_submission.id, :failed, nil, inspect(reason))

        StaConnectorWeb.Endpoint.broadcast(
          "files:#{file_submission.id}",
          "status_update",
          %{status: "failed", error: inspect(reason)}
        )

        {:error, reason}
    end
  end

  defp update_status(file_id, status, protocol \\ nil, error \\ nil) do
    StaConnector.Outbound.update_file_status(file_id, %{
      status: status,
      protocol: protocol,
      error_message: error
    })
  end
end
```

### Phoenix Channel for Real-time Updates

```elixir
defmodule StaConnectorWeb.FileChannel do
  use StaConnectorWeb, :channel

  @impl true
  def join("files:" <> file_id, _params, socket) do
    case StaConnector.Outbound.get_file(file_id) do
      {:ok, file} ->
        {:ok, %{status: file.status}, assign(socket, :file_id, file_id)}

      {:error, :not_found} ->
        {:error, %{reason: "file not found"}}
    end
  end

  @impl true
  def handle_info({:status_update, payload}, socket) do
    push(socket, "status_update", payload)
    {:noreply, socket}
  end
end
```

## Best Practices

1. **Use unique file names** - Include timestamp and sequence number
2. **Store the file ID** - For status tracking and troubleshooting
3. **Handle rate limits gracefully** - Implement backoff in your client
4. **Monitor queue depth** - Alert if pending files grow
5. **Implement idempotency** - Use metadata for deduplication
6. **Validate before sending** - Check file format locally
7. **Use Phoenix Channels** - For real-time status updates instead of polling

## BCB Rate Limits

BCB enforces strict rate limits:

| Limit | Value |
|-------|-------|
| Max Concurrent Uploads | 10 |
| Max Queries per Minute | 120 |

The connector automatically queues uploads when limits are reached.

## Next Steps

- [Inbound Pipeline](/guide/inbound) - Receiving files from BCB
- [Admin API](/api/admin) - Management and monitoring
- [Outbound API Reference](/api/outbound) - Full API documentation
