Skip to content

Exchange API

A Exchange API fornece serviços de conversão de moedas para o domínio store.
Ela permite consultar a taxa de câmbio entre duas moedas (from_currto_curr), aplicando automaticamente o spread configurado e vinculando a operação ao usuário autenticado.

Trusted layer e segurança

Toda requisição externa entra pelo gateway.
As rotas /exchange/** são protegidas: é obrigatório enviar Authorization: Bearer <jwt>.


Visão geral

  • Service (exchange-service): Microserviço em FastAPI (Python) que consulta um provedor externo de câmbio (HTTP), aplica um spread configurável e retorna as cotações.
classDiagram
    class ExchangeService {
        +getExchange(from: String, to: String): QuoteOut
    }

    class QuoteOut {
        -Double sell
        -Double buy
        -String date
        -String idAccount
    }

Estrutura da requisição

flowchart LR
    subgraph api [Trusted Layer]
        direction TB
        gateway --> account
        gateway --> auth
        account --> db@{ shape: cyl, label: "Database" }
        auth --> account
        gateway e1@==> exchange:::red
        gateway --> product
        gateway --> order
        product --> db
        order --> db
        order --> product
    end
    exchange e3@==> 3partyapi:::green@{label: "3rd-party API"}
    internet e2@==>|request| gateway
    e1@{ animate: true }
    e2@{ animate: true }
    e3@{ animate: true }
    classDef red fill:#fcc
    classDef green fill:#cfc
    click product "#product-api" "Product API"

Exchange-Service

📁 api/
└── 📁 exchange-service/
    ├── 📁 app/
       ├── 📄 __init__.py
       ├── 📄 main.py
       ├── 📄 auth.py
       ├── 📄 config.py
       ├── 📄 models.py
       └── 📁 clients/
           ├── 📄 __init__.py
           └── 📄 rates.py
    ├── 📄 requirements.txt
    └── 📄 Dockerfile
Source
1
2
3
4
5
6
fastapi==0.115.5
uvicorn[standard]==0.30.6
httpx==0.27.2
python-jose==3.3.0
pydantic-settings==2.6.1
PyJWT==2.9.0
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

ENV EXCHANGE_PORT=8080
EXPOSE 8080

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
from fastapi import FastAPI, Depends, HTTPException
from app.clients.rates import fetch_rate
from app.models import QuoteOut
from app.auth import require_auth
from app.config import settings

app = FastAPI(title="Exchange API", version="1.0.0")

@app.get("/exchange/{from_curr}/{to_curr}", response_model=QuoteOut)
async def get_exchange(from_curr: str, to_curr: str, claims: dict = Depends(require_auth)):
    try:
        rate, date = await fetch_rate(from_curr, to_curr)
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"rate provider error: {e!s}")

    half = settings.spread / 2.0
    sell = round(rate * (1 + half), 6)
    buy  = round(rate * (1 - half), 6)

    account_id = claims["id-account"]
    return {"sell": sell, "buy": buy, "date": date, "id-account": str(account_id)}
from fastapi import Request, HTTPException
import jwt
from app.config import settings

async def require_auth(request: Request) -> dict:
    # 1) Preferir o header que o gateway já inseriu
    account_id = request.headers.get("id-account")
    if account_id:
        return {"id-account": str(account_id)}

    # 2) Fallback: pegar do Bearer token
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="missing bearer token")

    token = auth.split()[1]
    try:
        claims = jwt.decode(
            token,
            settings.jwt_secret,
            algorithms=["HS512"],
            options={"verify_aud": False},
        )
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="invalid token")

    account_id = claims.get("id-account") or claims.get("id") or claims.get("sub")
    if not account_id:
        raise HTTPException(status_code=400, detail="missing account id")

    return {"id-account": str(account_id)}
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    # porta do uvicorn (só para rodar local)
    port: int = 8083

    # API de câmbio (sem chave por padrão)
    rates_base_url: str = "https://api.exchangerate.host"

    # spread para calcular buy/sell (ex.: 0.02 = ±1% em cada lado)
    spread: float = 0.02

    # verificação de JWT (opcional). Se não informar, só decodifica sem validar assinatura
    jwt_secret: str | None = None          # HS256
    jwt_public_key: str | None = None      # RS256/ES256 (PEM)
    jwt_algorithm: str | None = None       # "HS256", "RS256", ...

    class Config:
        env_prefix = "EXCHANGE_"
        env_file = ".env"

settings = Settings()
from pydantic import BaseModel, Field

class QuoteOut(BaseModel):
    sell: float
    buy: float
    date: str
    id_account: str = Field(..., alias="id-account")

    class Config:
        # garante que o FastAPI use o alias no JSON de saída
        populate_by_name = True
        orm_mode = True
import datetime as dt
import httpx

async def fetch_rate(from_curr: str, to_curr: str) -> tuple[float, str]:
    """
    Busca a cotação de from_curr->to_curr em provedores públicos e
    entende alguns formatos de resposta diferentes.
    Retorna (rate, date_iso). Lança RuntimeError se não conseguir extrair.
    """
    f = from_curr.upper()
    t = to_curr.upper()

    # 1) exchangerate.host (forma A: /convert)
    url_a = f"https://api.exchangerate.host/convert?from={f}&to={t}"
    # 2) frankfurter.app (forma B: /latest?from=&to=)
    url_b = f"https://api.frankfurter.app/latest?from={f}&to={t}"
    # 3) awesomeapi (forma C: /{from}-{to}/1) – retorna lista
    url_c = f"https://economia.awesomeapi.com.br/json/last/{f}-{t}"

    async with httpx.AsyncClient(timeout=10) as client:
        # tente A
        try:
            r = await client.get(url_a)
            data = r.json()
            # esperado: {"success":true, "info":{"rate": X}, "date":"YYYY-MM-DD", ...}
            rate = float(data.get("info", {}).get("rate"))
            date = data.get("date") or dt.date.today().isoformat()
            if rate > 0:
                return rate, date
        except Exception:
            pass

        # tente B
        try:
            r = await client.get(url_b)
            data = r.json()
            # esperado: {"amount":1,"base":"USD","date":"YYYY-MM-DD","rates":{"BRL":5.1}}
            rates = data.get("rates") or {}
            if t in rates:
                rate = float(rates[t])
                date = data.get("date") or dt.date.today().isoformat()
                if rate > 0:
                    return rate, date
        except Exception:
            pass

        # tente C
        try:
            r = await client.get(url_c)
            data = r.json()
            # esperado: {"USDBRL":{"bid":"5.18", "create_date":"2025-10-04 16:30:00", ...}}
            key = f"{f}{t}"
            if isinstance(data, dict) and key in data:
                item = data[key]
                rate = float(item.get("bid") or 0)
                date = (item.get("create_date") or item.get("timestamp")
                        or dt.date.today().isoformat())
                if rate > 0:
                    return rate, date
        except Exception:
            pass

    # nada deu certo
    raise RuntimeError("Provider schema not recognized or provider error")