::: info Ferramenta de exploração local Este simulador é uma ferramenta de teste rápido para entender o funcionamento da API. Não é um SDK e não deve ser usado em produção. O propósito é "rode e veja a API responder em 2 minutos, sem montar requests à mão". :::
O simulador é um único script (sem dependências externas além do runtime) que cobre todos os endpoints da API v2 de Parceiros: descoberta de identidade, webhooks, placas, transações e pedidos. Está disponível em 5 linguagens, todas com a mesma experiência: mesmo menu, mesmo formato de output, mesmo .env.
| Linguagem | Runtime mínimo |
|---|---|
| Node.js | Node 18+ |
| Python | Python 3.8+ |
| Go | Go 1.21+ |
| Java | Java 11+ |
| Bash | Bash 4+ + curl + jq |
Você também precisa de credenciais de homologação. Veja Sandbox para solicitar.
mkdir simulador-movvia && cd simulador-movviaEscolha uma das linguagens abaixo e copie o conteúdo do code block para o arquivo correspondente.
#!/usr/bin/env node
// Utilitário de teste para API Movvia Arrecada+ (PE)
// Requer Node.js 18+
import { createInterface } from 'readline';
import { readFileSync, existsSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dir = dirname(fileURLToPath(import.meta.url));
// ─── Carregar .env ────────────────────────────────────────────────────────────
const envPath = resolve(__dir, '.env');
if (existsSync(envPath)) {
for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
const t = line.trim();
if (!t || t.startsWith('#')) continue;
const eq = t.indexOf('=');
if (eq < 1) continue;
const key = t.slice(0, eq).trim();
const val = t.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
if (key && !process.env[key]) process.env[key] = val;
}
}
const BASE_URL =
process.env.BASE_URL ||
'https://hml.api.pedagioeletronico.com.br/gestao-webhooks-api/v1';
// ─── Identidade do parceiro ───────────────────────────────────────────────────
// Resolvido no startup: lê do .env (PE_PARCEIRO_ID) ou chama GET /me
let cachedParceiroId = process.env.PE_PARCEIRO_ID || null;
async function resolveParceiroId() {
if (cachedParceiroId) return cachedParceiroId;
const res = await fetch(`${BASE_URL}/me`, {
method: 'GET',
headers: { Authorization: authHeader(), 'Content-Type': 'application/json' },
});
const json = await res.json().catch(() => null);
if (!res.ok || !json?.data?.parceiroId) {
console.error('❌ Não foi possível descobrir o parceiroId via GET /me');
console.error(' ', JSON.stringify(json));
process.exit(1);
}
cachedParceiroId = String(json.data.parceiroId);
console.log(` ℹ️ parceiroId descoberto via GET /me: ${cachedParceiroId}`);
return cachedParceiroId;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function authHeader() {
const u = process.env.PE_USERNAME;
const p = process.env.PE_PASSWORD;
if (!u || !p) {
console.error('\n❌ Configure PE_USERNAME e PE_PASSWORD no arquivo .env');
console.error(' Copie .env.example → .env e preencha as credenciais.\n');
process.exit(1);
}
return 'Basic ' + Buffer.from(`${u}:${p}`).toString('base64');
}
async function req(method, path, body, queryParams, customHeaders = {}) {
let url = `${BASE_URL}${path}`;
if (queryParams) {
const qs = new URLSearchParams(
Object.entries(queryParams).filter(([, v]) => v !== '' && v != null)
).toString();
if (qs) url += '?' + qs;
}
// GET /me não precisa de x-parceiro-id (parceiro é inferido do Basic Auth)
const headers = { Authorization: authHeader(), 'Content-Type': 'application/json' };
if (path !== '/me') headers['x-parceiro-id'] = await resolveParceiroId();
Object.assign(headers, customHeaders);
const options = { method, headers };
if (body) options.body = JSON.stringify(body);
console.log(`\n→ ${method} ${url}`);
if (body) console.log(' Body:', JSON.stringify(body));
if (Object.keys(customHeaders).length) console.log(' Headers extras:', JSON.stringify(customHeaders));
let res;
try {
res = await fetch(url, options);
} catch (e) {
console.error('❌ Erro de rede:', e.message);
return null;
}
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch { json = text; }
console.log(`${res.ok ? '✅' : '❌'} ${res.status} ${res.statusText}`);
console.log(JSON.stringify(json, null, 2));
return { ok: res.ok, status: res.status, json };
}
// ─── Prompt readline ─────────────────────────────────────────────────────────
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ask = (q) => new Promise((r) => rl.question(q, r));
const askOpt = async (q) => { const v = (await ask(q)).trim(); return v || null; };
// ─── Ações ───────────────────────────────────────────────────────────────────
async function descobrirParceiro() {
await req('GET', '/me');
}
const EVENTOS_STATIC = [
'pe.pedido.criado',
'pe.pedido.confirmado',
'pe.pedido.cancelado',
'pe.pedido.expirado',
'pe.pedido.erro',
'pe.transacao.recebida',
'pe.transacao.atualizada',
'pe.transacao.cancelada',
];
async function listarEventosDisponiveis() {
return await req('GET', '/webhook/eventos');
}
async function cadastrarWebhook() {
const url = (await ask(' URL HTTPS do endpoint: ')).trim();
const chaveSecreta = (await ask(' Chave secreta HMAC: ')).trim();
console.log('\n Buscando eventos disponíveis...');
const res = await listarEventosDisponiveis();
let eventos = EVENTOS_STATIC;
if (res && res.ok && Array.isArray(res.json?.data)) {
eventos = res.json.data.map((e) => (typeof e === 'string' ? e : e.evento));
}
console.log('\n Eventos disponíveis:');
eventos.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
const rawChoice = (await ask('\n Escolha o número do evento ou digite o nome: ')).trim();
const idx = parseInt(rawChoice, 10);
const tipo = (!isNaN(idx) && idx > 0 && idx <= eventos.length)
? eventos[idx - 1]
: rawChoice || (await ask(' Tipo do evento: ')).trim();
if (!tipo) { console.log('❌ Tipo inválido.'); return; }
await req('POST', '/webhook', { url, chaveSecreta, tipo });
}
async function listarWebhooks() {
await req('GET', '/webhook');
}
async function testarConectividadeWebhook() {
const tipo = (await ask(' Tipo do evento (ex: pe.pedido.confirmado): ')).trim();
if (!tipo) return;
await req('POST', `/webhook/teste-conectividade/${encodeURIComponent(tipo)}`);
}
async function removerWebhook() {
const tipo = (await ask(' Tipo do webhook a remover (ex: pe.pedido.confirmado): ')).trim();
if (!tipo) return;
await req('DELETE', `/webhook/${encodeURIComponent(tipo)}`);
}
async function cadastrarPlaca() {
const placa = (await ask(' Placa (ex: ABC1D23): ')).trim().toUpperCase();
const dataInicio = await askOpt(' Data início monitoramento ISO 8601 (Enter p/ pular): ');
const dataFim = await askOpt(' Data fim monitoramento ISO 8601 (Enter p/ pular): ');
const body = { placa };
if (dataInicio) body.dataInicioMonitoramento = dataInicio;
if (dataFim) body.dataFimMonitoramento = dataFim;
await req('POST', '/placas', body);
}
async function removerPlaca() {
const placa = (await ask(' Placa a remover (ex: ABC1D23): ')).trim().toUpperCase();
await req('DELETE', `/placas/${placa}`);
}
async function listarTransacoes() {
const placa = await askOpt(' Filtrar por placa (Enter p/ pular): ');
const status = await askOpt(' Status [PENDENTE/PAGO/CANCELADA] (Enter p/ pular): ');
const dataInicio = await askOpt(' Data início ISO 8601 (Enter p/ pular): ');
const dataFim = await askOpt(' Data fim ISO 8601 (Enter p/ pular): ');
const pagina = (await askOpt(' Página (Enter = 0): ')) || '0';
const tamanhoPagina = (await askOpt(' Tamanho página (Enter = 50): ')) || '50';
await req('GET', '/transacoes', null, { placa, status, dataInicio, dataFim, pagina, tamanhoPagina });
}
async function criarPedido() {
const raw = (await ask(' IDs de transações separados por vírgula: ')).trim();
const transacoes = raw.split(',').map((s) => ({ transacaoId: s.trim() })).filter((t) => t.transacaoId);
const idempotencia = await askOpt(' Chave de idempotência (Opcional): ');
const headers = idempotencia ? { 'x-chave-idempotencia': idempotencia } : {};
await req('POST', '/pedidos', { transacoes }, null, headers);
}
async function consultarPedido() {
const pedidoId = (await ask(' ID do pedido: ')).trim();
await req('GET', `/pedidos/${pedidoId}`);
}
async function confirmarPagamento() {
const pedidoId = (await ask(' ID do pedido: ')).trim();
const referenciaExterna = await askOpt(' Referência externa (Enter p/ pular): ');
const dataLiquidacaoExterna = await askOpt(' Data liquidação externa ISO 8601 (Enter p/ pular): ');
const idempotencia = await askOpt(' Chave de idempotência (Opcional): ');
const body = {};
if (referenciaExterna) body.referenciaExterna = referenciaExterna;
if (dataLiquidacaoExterna) body.dataLiquidacaoExterna = dataLiquidacaoExterna;
const headers = idempotencia ? { 'x-chave-idempotencia': idempotencia } : {};
await req('POST', `/pedidos/${pedidoId}/confirmar`, Object.keys(body).length ? body : undefined, null, headers);
}
async function cancelarPedido() {
const pedidoId = (await ask(' ID do pedido: ')).trim();
const motivo = await askOpt(' Motivo [DESISTENCIA_PARCEIRO/ERRO_INTERNO/OUTROS] (Enter p/ pular): ');
const observacao = await askOpt(' Observação até 500 chars (Enter p/ pular): ');
const idempotencia = await askOpt(' Chave de idempotência (Opcional): ');
const body = {};
if (motivo) body.motivo = motivo.toUpperCase();
if (observacao) body.observacao = observacao;
const headers = idempotencia ? { 'x-chave-idempotencia': idempotencia } : {};
await req('POST', `/pedidos/${pedidoId}/cancelar`, Object.keys(body).length ? body : undefined, null, headers);
}
// ─── Menu ─────────────────────────────────────────────────────────────────────
const MENU = [
{ label: '── Primeiros Passos ─────────────────', action: null },
{ label: 'Descobrir parceiroId (GET /me)', action: descobrirParceiro },
{ label: '── Webhooks Config ──────────────────', action: null },
{ label: 'Listar eventos disponíveis', action: listarEventosDisponiveis },
{ label: 'Cadastrar webhook', action: cadastrarWebhook },
{ label: 'Listar webhooks', action: listarWebhooks },
{ label: 'Testar conectividade webhook', action: testarConectividadeWebhook },
{ label: 'Remover webhook', action: removerWebhook },
{ label: '── Placas ───────────────────────────', action: null },
{ label: 'Cadastrar placa', action: cadastrarPlaca },
{ label: 'Remover placa', action: removerPlaca },
{ label: '── Transações ───────────────────────', action: null },
{ label: 'Listar transações', action: listarTransacoes },
{ label: '── Pedidos ──────────────────────────', action: null },
{ label: 'Criar pedido', action: criarPedido },
{ label: 'Consultar pedido', action: consultarPedido },
{ label: 'Confirmar pagamento', action: confirmarPagamento },
{ label: 'Cancelar pedido', action: cancelarPedido },
];
function printMenu() {
console.log('\n╔══════════════════════════════════════════════╗');
console.log('║ Movvia Arrecada+ — PE API Tester ║');
const urlShort = BASE_URL.replace('https://', '');
const urlLine = urlShort.length > 43 ? urlShort.slice(0, 40) + '...' : urlShort.padEnd(43);
console.log(`║ ${urlLine}║`);
const pid = cachedParceiroId ? `parceiroId: ${cachedParceiroId}` : 'parceiroId: (auto via GET /me)';
console.log(`║ ${pid.padEnd(43)}║`);
console.log('╠══════════════════════════════════════════════╣');
let idx = 1;
for (const item of MENU) {
if (!item.action) {
console.log(`║ ${item.label.padEnd(44)}║`);
} else {
console.log(`║ ${String(idx).padStart(2)}. ${item.label.padEnd(39)}║`);
idx++;
}
}
console.log('║ ║');
console.log('║ 0. Sair ║');
console.log('╚══════════════════════════════════════════════╝');
}
async function main() {
// Passo zero: resolver parceiroId antes de qualquer interação com o menu
if (!cachedParceiroId) {
console.log('\n ℹ️ PE_PARCEIRO_ID não configurado — chamando GET /me para descobrir...');
await resolveParceiroId();
}
const actions = MENU.filter((m) => m.action).map((m) => m.action);
while (true) {
printMenu();
const choice = (await ask('\nEscolha uma opção: ')).trim();
if (choice === '0' || choice.toLowerCase() === 'sair') break;
const n = parseInt(choice, 10);
if (isNaN(n) || n < 1 || n > actions.length) {
console.log('Opção inválida.');
continue;
}
try {
await actions[n - 1]();
} catch (e) {
console.error('Erro inesperado:', e.message);
}
await ask('\n[Enter para voltar ao menu]');
}
console.log('\nAté logo!\n');
rl.close();
}
main();#!/usr/bin/env python3
# Utilitário de teste para API Movvia Arrecada+ (PE)
# Requer Python 3.8+
import base64
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
# ─── Carregar .env ────────────────────────────────────────────────────────────
ENV_PATH = Path(__file__).resolve().parent / ".env"
if ENV_PATH.exists():
for line in ENV_PATH.read_text(encoding="utf-8").splitlines():
t = line.strip()
if not t or t.startswith("#") or "=" not in t:
continue
key, val = t.split("=", 1)
key = key.strip()
val = val.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = val
BASE_URL = os.environ.get("BASE_URL") or "https://hml.api.pedagioeletronico.com.br/gestao-webhooks-api/v1"
# ─── Identidade do parceiro ───────────────────────────────────────────────────
cached_parceiro_id = os.environ.get("PE_PARCEIRO_ID") or None
def auth_header() -> str:
u = os.environ.get("PE_USERNAME")
p = os.environ.get("PE_PASSWORD")
if not u or not p:
print("\n❌ Configure PE_USERNAME e PE_PASSWORD no arquivo .env")
print(" Copie .env.example → .env e preencha as credenciais.\n")
sys.exit(1)
raw = f"{u}:{p}".encode("utf-8")
return "Basic " + base64.b64encode(raw).decode("ascii")
def resolve_parceiro_id() -> str:
global cached_parceiro_id
if cached_parceiro_id:
return cached_parceiro_id
res = req("GET", "/me", quiet=True)
if not res or not res.get("ok"):
print("❌ Não foi possível descobrir o parceiroId via GET /me")
sys.exit(1)
body = res.get("json")
pid = (body or {}).get("data", {}).get("parceiroId") if isinstance(body, dict) else None
if not pid:
print("❌ Resposta sem data.parceiroId")
sys.exit(1)
cached_parceiro_id = str(pid)
print(f" ℹ️ parceiroId descoberto via GET /me: {cached_parceiro_id}")
return cached_parceiro_id
def req(method, path, body=None, query=None, headers=None, quiet=False):
url = BASE_URL + path
if query:
items = [(k, v) for k, v in query.items() if v not in (None, "")]
if items:
url += "?" + urllib.parse.urlencode(items)
h = {"Authorization": auth_header(), "Content-Type": "application/json"}
if path != "/me":
h["x-parceiro-id"] = resolve_parceiro_id()
if headers:
h.update(headers)
data = json.dumps(body).encode("utf-8") if body is not None else None
if not quiet:
print(f"\n→ {method} {url}")
if body is not None:
print(f" Body: {json.dumps(body, ensure_ascii=False)}")
if headers:
print(f" Headers extras: {json.dumps(headers, ensure_ascii=False)}")
request = urllib.request.Request(url, data=data, method=method, headers=h)
try:
with urllib.request.urlopen(request) as resp:
status = resp.status
text = resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
status = e.code
text = e.read().decode("utf-8")
except Exception as e:
print(f"❌ Erro de rede: {e}")
return None
try:
parsed = json.loads(text)
except Exception:
parsed = text
if not quiet:
ok = 200 <= status < 400
print(f"{'✅' if ok else '❌'} {status}")
if isinstance(parsed, (dict, list)):
print(json.dumps(parsed, ensure_ascii=False, indent=2))
else:
print(parsed)
return {"ok": 200 <= status < 400, "status": status, "json": parsed}
# ─── Prompts ──────────────────────────────────────────────────────────────────
def ask(q):
return input(q)
def ask_opt(q):
v = input(q).strip()
return v or None
# ─── Ações ────────────────────────────────────────────────────────────────────
def descobrir_parceiro():
req("GET", "/me")
EVENTOS_STATIC = [
"pe.pedido.criado",
"pe.pedido.confirmado",
"pe.pedido.cancelado",
"pe.pedido.expirado",
"pe.pedido.erro",
"pe.transacao.recebida",
"pe.transacao.atualizada",
"pe.transacao.cancelada",
]
def listar_eventos_disponiveis():
return req("GET", "/webhook/eventos")
def cadastrar_webhook():
url = ask(" URL HTTPS do endpoint: ").strip()
chave = ask(" Chave secreta HMAC: ").strip()
print("\n Buscando eventos disponíveis...")
res = listar_eventos_disponiveis()
eventos = EVENTOS_STATIC
if res and res.get("ok"):
body = res.get("json")
data = body.get("data") if isinstance(body, dict) else None
if isinstance(data, list) and data:
eventos = [e if isinstance(e, str) else (e.get("evento") if isinstance(e, dict) else None) for e in data]
eventos = [e for e in eventos if e]
print("\n Eventos disponíveis:")
for i, e in enumerate(eventos, start=1):
print(f" {i}. {e}")
raw = ask("\n Escolha o número do evento ou digite o nome: ").strip()
tipo = raw
try:
idx = int(raw)
if 1 <= idx <= len(eventos):
tipo = eventos[idx - 1]
except ValueError:
pass
if not tipo:
tipo = ask(" Tipo do evento: ").strip()
if not tipo:
print("❌ Tipo inválido.")
return
req("POST", "/webhook", {"url": url, "chaveSecreta": chave, "tipo": tipo})
def listar_webhooks():
req("GET", "/webhook")
def testar_conectividade_webhook():
tipo = ask(" Tipo do evento (ex: pe.pedido.confirmado): ").strip()
if not tipo:
return
req("POST", f"/webhook/teste-conectividade/{urllib.parse.quote(tipo, safe='')}")
def remover_webhook():
tipo = ask(" Tipo do webhook a remover (ex: pe.pedido.confirmado): ").strip()
if not tipo:
return
req("DELETE", f"/webhook/{urllib.parse.quote(tipo, safe='')}")
def cadastrar_placa():
placa = ask(" Placa (ex: ABC1D23): ").strip().upper()
di = ask_opt(" Data início monitoramento ISO 8601 (Enter p/ pular): ")
df = ask_opt(" Data fim monitoramento ISO 8601 (Enter p/ pular): ")
body = {"placa": placa}
if di:
body["dataInicioMonitoramento"] = di
if df:
body["dataFimMonitoramento"] = df
req("POST", "/placas", body)
def remover_placa():
placa = ask(" Placa a remover (ex: ABC1D23): ").strip().upper()
req("DELETE", f"/placas/{placa}")
def listar_transacoes():
placa = ask_opt(" Filtrar por placa (Enter p/ pular): ")
status = ask_opt(" Status [PENDENTE/PAGO/CANCELADA] (Enter p/ pular): ")
di = ask_opt(" Data início ISO 8601 (Enter p/ pular): ")
df = ask_opt(" Data fim ISO 8601 (Enter p/ pular): ")
pagina = ask_opt(" Página (Enter = 0): ") or "0"
tamanho = ask_opt(" Tamanho página (Enter = 50): ") or "50"
req("GET", "/transacoes", query={
"placa": placa, "status": status,
"dataInicio": di, "dataFim": df,
"pagina": pagina, "tamanhoPagina": tamanho,
})
def criar_pedido():
raw = ask(" IDs de transações separados por vírgula: ").strip()
transacoes = [{"transacaoId": s.strip()} for s in raw.split(",") if s.strip()]
idemp = ask_opt(" Chave de idempotência (Opcional): ")
headers = {"x-chave-idempotencia": idemp} if idemp else None
req("POST", "/pedidos", {"transacoes": transacoes}, headers=headers)
def consultar_pedido():
pid = ask(" ID do pedido: ").strip()
req("GET", f"/pedidos/{pid}")
def confirmar_pagamento():
pid = ask(" ID do pedido: ").strip()
ref = ask_opt(" Referência externa (Enter p/ pular): ")
dt = ask_opt(" Data liquidação externa ISO 8601 (Enter p/ pular): ")
idemp = ask_opt(" Chave de idempotência (Opcional): ")
body = {}
if ref:
body["referenciaExterna"] = ref
if dt:
body["dataLiquidacaoExterna"] = dt
headers = {"x-chave-idempotencia": idemp} if idemp else None
req("POST", f"/pedidos/{pid}/confirmar", body or None, headers=headers)
def cancelar_pedido():
pid = ask(" ID do pedido: ").strip()
motivo = ask_opt(" Motivo [DESISTENCIA_PARCEIRO/ERRO_INTERNO/OUTROS] (Enter p/ pular): ")
obs = ask_opt(" Observação até 500 chars (Enter p/ pular): ")
idemp = ask_opt(" Chave de idempotência (Opcional): ")
body = {}
if motivo:
body["motivo"] = motivo.upper()
if obs:
body["observacao"] = obs
headers = {"x-chave-idempotencia": idemp} if idemp else None
req("POST", f"/pedidos/{pid}/cancelar", body or None, headers=headers)
# ─── Menu ─────────────────────────────────────────────────────────────────────
MENU = [
("── Primeiros Passos ─────────────────", None),
("Descobrir parceiroId (GET /me)", descobrir_parceiro),
("── Webhooks Config ──────────────────", None),
("Listar eventos disponíveis", listar_eventos_disponiveis),
("Cadastrar webhook", cadastrar_webhook),
("Listar webhooks", listar_webhooks),
("Testar conectividade webhook", testar_conectividade_webhook),
("Remover webhook", remover_webhook),
("── Placas ───────────────────────────", None),
("Cadastrar placa", cadastrar_placa),
("Remover placa", remover_placa),
("── Transações ───────────────────────", None),
("Listar transações", listar_transacoes),
("── Pedidos ──────────────────────────", None),
("Criar pedido", criar_pedido),
("Consultar pedido", consultar_pedido),
("Confirmar pagamento", confirmar_pagamento),
("Cancelar pedido", cancelar_pedido),
]
def print_menu():
print("\n╔══════════════════════════════════════════════╗")
print("║ Movvia Arrecada+ — PE API Tester ║")
short = BASE_URL.replace("https://", "")
line = (short[:40] + "...") if len(short) > 43 else short.ljust(43)
print(f"║ {line}║")
pid = f"parceiroId: {cached_parceiro_id}" if cached_parceiro_id else "parceiroId: (auto via GET /me)"
print(f"║ {pid.ljust(43)}║")
print("╠══════════════════════════════════════════════╣")
idx = 1
for label, action in MENU:
if action is None:
print(f"║ {label.ljust(44)}║")
else:
print(f"║ {str(idx).rjust(2)}. {label.ljust(39)}║")
idx += 1
print("║ ║")
print("║ 0. Sair ║")
print("╚══════════════════════════════════════════════╝")
def main():
if not cached_parceiro_id:
print("\n ℹ️ PE_PARCEIRO_ID não configurado — chamando GET /me para descobrir...")
resolve_parceiro_id()
actions = [a for _, a in MENU if a is not None]
while True:
print_menu()
choice = input("\nEscolha uma opção: ").strip().lower()
if choice in ("0", "sair"):
break
try:
n = int(choice)
except ValueError:
print("Opção inválida.")
continue
if n < 1 or n > len(actions):
print("Opção inválida.")
continue
try:
actions[n - 1]()
except Exception as e:
print(f"Erro inesperado: {e}")
input("\n[Enter para voltar ao menu]")
print("\nAté logo!\n")
if __name__ == "__main__":
main()// Utilitário de teste para API Movvia Arrecada+ (PE)
// Requer Go 1.21+. Roda com: go run simulador.go
package main
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
)
var (
baseURL = ""
cachedParceiroID = ""
stdin = bufio.NewReader(os.Stdin)
)
// ─── Carregar .env ────────────────────────────────────────────────────────────
func loadEnv() {
data, err := os.ReadFile(".env")
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
t := strings.TrimSpace(line)
if t == "" || strings.HasPrefix(t, "#") {
continue
}
idx := strings.Index(t, "=")
if idx < 1 {
continue
}
key := strings.TrimSpace(t[:idx])
val := strings.TrimSpace(t[idx+1:])
val = strings.Trim(val, "\"'")
if key != "" && os.Getenv(key) == "" {
os.Setenv(key, val)
}
}
}
func getenvDefault(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
}
return d
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
func authHeader() string {
u := os.Getenv("PE_USERNAME")
p := os.Getenv("PE_PASSWORD")
if u == "" || p == "" {
fmt.Println("\n❌ Configure PE_USERNAME e PE_PASSWORD no arquivo .env")
fmt.Println(" Copie .env.example → .env e preencha as credenciais.")
fmt.Println()
os.Exit(1)
}
return "Basic " + base64.StdEncoding.EncodeToString([]byte(u+":"+p))
}
type result struct {
ok bool
status int
body string
}
func req(method, path string, body any, query map[string]string, headers map[string]string, quiet bool) *result {
u := baseURL + path
if len(query) > 0 {
q := url.Values{}
for k, v := range query {
if v != "" {
q.Set(k, v)
}
}
if encoded := q.Encode(); encoded != "" {
u += "?" + encoded
}
}
var bodyReader io.Reader
var bodyJSON []byte
if body != nil {
var err error
bodyJSON, err = json.Marshal(body)
if err != nil {
fmt.Printf("❌ Erro ao serializar body: %v\n", err)
return nil
}
bodyReader = bytes.NewReader(bodyJSON)
}
r, err := http.NewRequest(method, u, bodyReader)
if err != nil {
fmt.Printf("❌ Erro ao montar request: %v\n", err)
return nil
}
r.Header.Set("Authorization", authHeader())
r.Header.Set("Content-Type", "application/json")
if path != "/me" {
r.Header.Set("x-parceiro-id", resolveParceiroID())
}
for k, v := range headers {
r.Header.Set(k, v)
}
if !quiet {
fmt.Printf("\n→ %s %s\n", method, u)
if bodyJSON != nil {
fmt.Printf(" Body: %s\n", string(bodyJSON))
}
if len(headers) > 0 {
h, _ := json.Marshal(headers)
fmt.Printf(" Headers extras: %s\n", string(h))
}
}
resp, err := http.DefaultClient.Do(r)
if err != nil {
fmt.Printf("❌ Erro de rede: %v\n", err)
return nil
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if !quiet {
ok := resp.StatusCode < 400
mark := "❌"
if ok {
mark = "✅"
}
fmt.Printf("%s %d %s\n", mark, resp.StatusCode, http.StatusText(resp.StatusCode))
var pretty bytes.Buffer
if err := json.Indent(&pretty, raw, "", " "); err == nil {
fmt.Println(pretty.String())
} else {
fmt.Println(string(raw))
}
}
return &result{ok: resp.StatusCode < 400, status: resp.StatusCode, body: string(raw)}
}
func resolveParceiroID() string {
if cachedParceiroID != "" {
return cachedParceiroID
}
r := req("GET", "/me", nil, nil, nil, true)
if r == nil || !r.ok {
fmt.Println("❌ Não foi possível descobrir o parceiroId via GET /me")
os.Exit(1)
}
var parsed struct {
Data struct {
ParceiroID json.RawMessage `json:"parceiroId"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(r.body), &parsed); err != nil {
fmt.Printf("❌ Erro ao parsear resposta de /me: %v\n", err)
os.Exit(1)
}
pid := strings.Trim(string(parsed.Data.ParceiroID), `"`)
if pid == "" || pid == "null" {
fmt.Println("❌ Resposta sem data.parceiroId")
os.Exit(1)
}
cachedParceiroID = pid
fmt.Printf(" ℹ️ parceiroId descoberto via GET /me: %s\n", pid)
return pid
}
// ─── Prompts ──────────────────────────────────────────────────────────────────
func prompt(p string) string {
fmt.Print(p)
line, _ := stdin.ReadString('\n')
return strings.TrimRight(line, "\r\n")
}
func promptOpt(p string) string {
return strings.TrimSpace(prompt(p))
}
// ─── Ações ───────────────────────────────────────────────────────────────────
func descobrirParceiro() { req("GET", "/me", nil, nil, nil, false) }
func listarWebhooks() { req("GET", "/webhook", nil, nil, nil, false) }
func listarEventosDisponiveis() *result {
return req("GET", "/webhook/eventos", nil, nil, nil, false)
}
var eventosStatic = []string{
"pe.pedido.criado",
"pe.pedido.confirmado",
"pe.pedido.cancelado",
"pe.pedido.expirado",
"pe.pedido.erro",
"pe.transacao.recebida",
"pe.transacao.atualizada",
"pe.transacao.cancelada",
}
func cadastrarWebhook() {
urlStr := strings.TrimSpace(prompt(" URL HTTPS do endpoint: "))
chave := strings.TrimSpace(prompt(" Chave secreta HMAC: "))
fmt.Println("\n Buscando eventos disponíveis...")
res := listarEventosDisponiveis()
eventos := eventosStatic
if res != nil && res.ok {
var parsed struct {
Data []json.RawMessage `json:"data"`
}
if err := json.Unmarshal([]byte(res.body), &parsed); err == nil && len(parsed.Data) > 0 {
ev := []string{}
for _, e := range parsed.Data {
s := string(e)
if strings.HasPrefix(s, "{") {
var obj struct {
Evento string `json:"evento"`
}
if json.Unmarshal(e, &obj) == nil && obj.Evento != "" {
ev = append(ev, obj.Evento)
}
} else {
var str string
if json.Unmarshal(e, &str) == nil && str != "" {
ev = append(ev, str)
}
}
}
if len(ev) > 0 {
eventos = ev
}
}
}
fmt.Println("\n Eventos disponíveis:")
for i, e := range eventos {
fmt.Printf(" %d. %s\n", i+1, e)
}
raw := strings.TrimSpace(prompt("\n Escolha o número do evento ou digite o nome: "))
tipo := raw
if n, err := strconv.Atoi(raw); err == nil && n >= 1 && n <= len(eventos) {
tipo = eventos[n-1]
}
if tipo == "" {
tipo = strings.TrimSpace(prompt(" Tipo do evento: "))
}
if tipo == "" {
fmt.Println("❌ Tipo inválido.")
return
}
req("POST", "/webhook", map[string]any{
"url": urlStr,
"chaveSecreta": chave,
"tipo": tipo,
}, nil, nil, false)
}
func testarConectividadeWebhook() {
tipo := strings.TrimSpace(prompt(" Tipo do evento (ex: pe.pedido.confirmado): "))
if tipo == "" {
return
}
req("POST", "/webhook/teste-conectividade/"+url.PathEscape(tipo), nil, nil, nil, false)
}
func removerWebhook() {
tipo := strings.TrimSpace(prompt(" Tipo do webhook a remover (ex: pe.pedido.confirmado): "))
if tipo == "" {
return
}
req("DELETE", "/webhook/"+url.PathEscape(tipo), nil, nil, nil, false)
}
func cadastrarPlaca() {
placa := strings.ToUpper(strings.TrimSpace(prompt(" Placa (ex: ABC1D23): ")))
di := promptOpt(" Data início monitoramento ISO 8601 (Enter p/ pular): ")
df := promptOpt(" Data fim monitoramento ISO 8601 (Enter p/ pular): ")
body := map[string]any{"placa": placa}
if di != "" {
body["dataInicioMonitoramento"] = di
}
if df != "" {
body["dataFimMonitoramento"] = df
}
req("POST", "/placas", body, nil, nil, false)
}
func removerPlaca() {
placa := strings.ToUpper(strings.TrimSpace(prompt(" Placa a remover (ex: ABC1D23): ")))
req("DELETE", "/placas/"+placa, nil, nil, nil, false)
}
func listarTransacoes() {
q := map[string]string{
"placa": promptOpt(" Filtrar por placa (Enter p/ pular): "),
"status": promptOpt(" Status [PENDENTE/PAGO/CANCELADA] (Enter p/ pular): "),
"dataInicio": promptOpt(" Data início ISO 8601 (Enter p/ pular): "),
"dataFim": promptOpt(" Data fim ISO 8601 (Enter p/ pular): "),
}
pagina := promptOpt(" Página (Enter = 0): ")
if pagina == "" {
pagina = "0"
}
tamanho := promptOpt(" Tamanho página (Enter = 50): ")
if tamanho == "" {
tamanho = "50"
}
q["pagina"] = pagina
q["tamanhoPagina"] = tamanho
req("GET", "/transacoes", nil, q, nil, false)
}
func criarPedido() {
raw := strings.TrimSpace(prompt(" IDs de transações separados por vírgula: "))
transacoes := []map[string]string{}
for _, s := range strings.Split(raw, ",") {
s = strings.TrimSpace(s)
if s != "" {
transacoes = append(transacoes, map[string]string{"transacaoId": s})
}
}
idemp := promptOpt(" Chave de idempotência (Opcional): ")
headers := map[string]string{}
if idemp != "" {
headers["x-chave-idempotencia"] = idemp
}
req("POST", "/pedidos", map[string]any{"transacoes": transacoes}, nil, headers, false)
}
func consultarPedido() {
pid := strings.TrimSpace(prompt(" ID do pedido: "))
req("GET", "/pedidos/"+pid, nil, nil, nil, false)
}
func confirmarPagamento() {
pid := strings.TrimSpace(prompt(" ID do pedido: "))
ref := promptOpt(" Referência externa (Enter p/ pular): ")
dt := promptOpt(" Data liquidação externa ISO 8601 (Enter p/ pular): ")
idemp := promptOpt(" Chave de idempotência (Opcional): ")
body := map[string]any{}
if ref != "" {
body["referenciaExterna"] = ref
}
if dt != "" {
body["dataLiquidacaoExterna"] = dt
}
headers := map[string]string{}
if idemp != "" {
headers["x-chave-idempotencia"] = idemp
}
var bodyArg any
if len(body) > 0 {
bodyArg = body
}
req("POST", "/pedidos/"+pid+"/confirmar", bodyArg, nil, headers, false)
}
func cancelarPedido() {
pid := strings.TrimSpace(prompt(" ID do pedido: "))
motivo := promptOpt(" Motivo [DESISTENCIA_PARCEIRO/ERRO_INTERNO/OUTROS] (Enter p/ pular): ")
obs := promptOpt(" Observação até 500 chars (Enter p/ pular): ")
idemp := promptOpt(" Chave de idempotência (Opcional): ")
body := map[string]any{}
if motivo != "" {
body["motivo"] = strings.ToUpper(motivo)
}
if obs != "" {
body["observacao"] = obs
}
headers := map[string]string{}
if idemp != "" {
headers["x-chave-idempotencia"] = idemp
}
var bodyArg any
if len(body) > 0 {
bodyArg = body
}
req("POST", "/pedidos/"+pid+"/cancelar", bodyArg, nil, headers, false)
}
// ─── Menu ─────────────────────────────────────────────────────────────────────
type menuItem struct {
label string
action func()
}
func menu() []menuItem {
return []menuItem{
{"── Primeiros Passos ─────────────────", nil},
{"Descobrir parceiroId (GET /me)", descobrirParceiro},
{"── Webhooks Config ──────────────────", nil},
{"Listar eventos disponíveis", func() { listarEventosDisponiveis() }},
{"Cadastrar webhook", cadastrarWebhook},
{"Listar webhooks", listarWebhooks},
{"Testar conectividade webhook", testarConectividadeWebhook},
{"Remover webhook", removerWebhook},
{"── Placas ───────────────────────────", nil},
{"Cadastrar placa", cadastrarPlaca},
{"Remover placa", removerPlaca},
{"── Transações ───────────────────────", nil},
{"Listar transações", listarTransacoes},
{"── Pedidos ──────────────────────────", nil},
{"Criar pedido", criarPedido},
{"Consultar pedido", consultarPedido},
{"Confirmar pagamento", confirmarPagamento},
{"Cancelar pedido", cancelarPedido},
}
}
func padRight(s string, n int) string {
if len(s) >= n {
return s
}
return s + strings.Repeat(" ", n-len(s))
}
func printMenu() {
items := menu()
fmt.Println("\n╔══════════════════════════════════════════════╗")
fmt.Println("║ Movvia Arrecada+ — PE API Tester ║")
short := strings.Replace(baseURL, "https://", "", 1)
if len(short) > 43 {
short = short[:40] + "..."
}
fmt.Printf("║ %s║\n", padRight(short, 43))
pid := "parceiroId: (auto via GET /me)"
if cachedParceiroID != "" {
pid = "parceiroId: " + cachedParceiroID
}
fmt.Printf("║ %s║\n", padRight(pid, 43))
fmt.Println("╠══════════════════════════════════════════════╣")
idx := 1
for _, it := range items {
if it.action == nil {
fmt.Printf("║ %s║\n", padRight(it.label, 44))
} else {
line := fmt.Sprintf("%2d. %s", idx, it.label)
fmt.Printf("║ %s║\n", padRight(line, 43))
idx++
}
}
fmt.Println("║ ║")
fmt.Println("║ 0. Sair ║")
fmt.Println("╚══════════════════════════════════════════════╝")
}
func main() {
loadEnv()
baseURL = getenvDefault("BASE_URL", "https://hml.api.pedagioeletronico.com.br/gestao-webhooks-api/v1")
cachedParceiroID = os.Getenv("PE_PARCEIRO_ID")
if cachedParceiroID == "" {
fmt.Println("\n ℹ️ PE_PARCEIRO_ID não configurado — chamando GET /me para descobrir...")
resolveParceiroID()
}
items := menu()
actions := []func(){}
for _, it := range items {
if it.action != nil {
actions = append(actions, it.action)
}
}
for {
printMenu()
choice := strings.ToLower(strings.TrimSpace(prompt("\nEscolha uma opção: ")))
if choice == "0" || choice == "sair" {
break
}
n, err := strconv.Atoi(choice)
if err != nil || n < 1 || n > len(actions) {
fmt.Println("Opção inválida.")
continue
}
func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Erro inesperado: %v\n", r)
}
}()
actions[n-1]()
}()
prompt("\n[Enter para voltar ao menu]")
}
fmt.Println("\nAté logo!")
}// Utilitário de teste para API Movvia Arrecada+ (PE)
// Requer Java 11+. Roda com: java Simulador.java
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Simulador {
static final HttpClient HTTP = HttpClient.newHttpClient();
static final Scanner IN = new Scanner(System.in);
static final List<String> EVENTOS_STATIC = List.of(
"pe.pedido.criado", "pe.pedido.confirmado", "pe.pedido.cancelado",
"pe.pedido.expirado", "pe.pedido.erro",
"pe.transacao.recebida", "pe.transacao.atualizada", "pe.transacao.cancelada"
);
static String baseUrl;
static String cachedParceiroId;
static class Result {
final boolean ok;
final int status;
final String body;
Result(boolean ok, int status, String body) { this.ok = ok; this.status = status; this.body = body; }
}
// ─── Carregar .env ────────────────────────────────────────────────────────
static void loadEnv() {
Path p = Paths.get(".env");
if (!Files.exists(p)) return;
try {
for (String line : Files.readAllLines(p, StandardCharsets.UTF_8)) {
String t = line.trim();
if (t.isEmpty() || t.startsWith("#")) continue;
int eq = t.indexOf('=');
if (eq < 1) continue;
String key = t.substring(0, eq).trim();
String val = t.substring(eq + 1).trim().replaceAll("^[\"']|[\"']$", "");
if (!key.isEmpty() && env(key) == null) {
System.setProperty(key, val);
}
}
} catch (IOException ignored) { /* arquivo opcional */ }
}
static String env(String k) {
String v = System.getProperty(k);
if (v == null || v.isEmpty()) v = System.getenv(k);
return (v == null || v.isEmpty()) ? null : v;
}
static String envOr(String k, String d) {
String v = env(k);
return v == null ? d : v;
}
// ─── Helpers ──────────────────────────────────────────────────────────────
static String authHeader() {
String u = env("PE_USERNAME"), p = env("PE_PASSWORD");
if (u == null || p == null) {
System.out.println("\n❌ Configure PE_USERNAME e PE_PASSWORD no arquivo .env");
System.out.println(" Copie .env.example → .env e preencha as credenciais.\n");
System.exit(1);
}
return "Basic " + Base64.getEncoder().encodeToString((u + ":" + p).getBytes(StandardCharsets.UTF_8));
}
/** Constrói JSON inline a partir de pares chave/valor.
* Strings ganham aspas; numbers, booleans e null não. */
static String json(Object... kv) {
StringBuilder sb = new StringBuilder("{");
for (int i = 0; i < kv.length; i += 2) {
if (i > 0) sb.append(',');
sb.append('"').append(escape(String.valueOf(kv[i]))).append("\":").append(jsonValue(kv[i + 1]));
}
return sb.append('}').toString();
}
static String jsonValue(Object v) {
if (v == null) return "null";
if (v instanceof Number || v instanceof Boolean) return v.toString();
return "\"" + escape(String.valueOf(v)) + "\"";
}
static String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
// ─── Função abstratora única ──────────────────────────────────────────────
static Result req(String method, String path) { return req(method, path, null, null, null, false); }
static Result req(String method, String path, String body, Map<String,String> query,
Map<String,String> extraHeaders, boolean quiet) {
StringBuilder u = new StringBuilder(baseUrl).append(path);
if (query != null && !query.isEmpty()) {
StringBuilder qs = new StringBuilder();
for (Map.Entry<String,String> e : query.entrySet()) {
if (e.getValue() == null || e.getValue().isEmpty()) continue;
if (qs.length() > 0) qs.append('&');
qs.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
.append('=')
.append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
}
if (qs.length() > 0) u.append('?').append(qs);
}
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(u.toString()))
.header("Authorization", authHeader())
.header("Content-Type", "application/json");
if (!"/me".equals(path)) b.header("x-parceiro-id", resolveParceiroId());
if (extraHeaders != null) extraHeaders.forEach(b::header);
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8);
b.method(method, pub);
if (!quiet) {
System.out.println("\n→ " + method + " " + u);
if (body != null) System.out.println(" Body: " + body);
if (extraHeaders != null && !extraHeaders.isEmpty())
System.out.println(" Headers extras: " + extraHeaders);
}
try {
HttpResponse<String> resp = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
boolean ok = resp.statusCode() < 400;
if (!quiet) {
System.out.println((ok ? "✅" : "❌") + " " + resp.statusCode());
System.out.println(resp.body());
}
return new Result(ok, resp.statusCode(), resp.body());
} catch (IOException | InterruptedException e) {
System.out.println("❌ Erro de rede: " + e.getMessage());
return null;
}
}
static String resolveParceiroId() {
if (cachedParceiroId != null && !cachedParceiroId.isEmpty()) return cachedParceiroId;
Result r = req("GET", "/me", null, null, null, true);
if (r == null || !r.ok) {
System.out.println("❌ Não foi possível descobrir o parceiroId via GET /me");
System.exit(1);
}
Matcher m = Pattern.compile("\"parceiroId\"\\s*:\\s*\"([^\"]+)\"").matcher(r.body);
if (!m.find()) {
System.out.println("❌ Resposta sem data.parceiroId: " + r.body);
System.exit(1);
}
cachedParceiroId = m.group(1);
System.out.println(" ℹ️ parceiroId descoberto via GET /me: " + cachedParceiroId);
return cachedParceiroId;
}
// ─── Prompts ──────────────────────────────────────────────────────────────
static String prompt(String q) {
System.out.print(q);
return IN.hasNextLine() ? IN.nextLine() : "";
}
static String promptOpt(String q) {
String v = prompt(q).trim();
return v.isEmpty() ? null : v;
}
// ─── Ações ────────────────────────────────────────────────────────────────
static void descobrirParceiro() { req("GET", "/me"); }
static Result listarEventosDisponiveis() { return req("GET", "/webhook/eventos"); }
static void cadastrarWebhook() {
String url = prompt(" URL HTTPS do endpoint: ").trim();
String chave = prompt(" Chave secreta HMAC: ").trim();
System.out.println("\n Buscando eventos disponíveis...");
Result r = listarEventosDisponiveis();
List<String> eventos = EVENTOS_STATIC;
if (r != null && r.ok) {
List<String> parsed = new ArrayList<>();
// formato {data: [{evento: "..."}]}
Matcher me = Pattern.compile("\"evento\"\\s*:\\s*\"([^\"]+)\"").matcher(r.body);
while (me.find()) parsed.add(me.group(1));
// fallback: data: ["...", "..."]
if (parsed.isEmpty()) {
Matcher md = Pattern.compile("\"data\"\\s*:\\s*\\[([^\\]]+)\\]").matcher(r.body);
if (md.find()) {
Matcher ms = Pattern.compile("\"([^\"]+)\"").matcher(md.group(1));
while (ms.find()) parsed.add(ms.group(1));
}
}
if (!parsed.isEmpty()) eventos = parsed;
}
System.out.println("\n Eventos disponíveis:");
for (int i = 0; i < eventos.size(); i++)
System.out.println(" " + (i + 1) + ". " + eventos.get(i));
String raw = prompt("\n Escolha o número do evento ou digite o nome: ").trim();
String tipo = raw;
try {
int idx = Integer.parseInt(raw);
if (idx >= 1 && idx <= eventos.size()) tipo = eventos.get(idx - 1);
} catch (NumberFormatException ignored) {}
if (tipo.isEmpty()) tipo = prompt(" Tipo do evento: ").trim();
if (tipo.isEmpty()) { System.out.println("❌ Tipo inválido."); return; }
req("POST", "/webhook", json("url", url, "chaveSecreta", chave, "tipo", tipo), null, null, false);
}
static void listarWebhooks() { req("GET", "/webhook"); }
static void testarConectividadeWebhook() {
String tipo = prompt(" Tipo do evento (ex: pe.pedido.confirmado): ").trim();
if (tipo.isEmpty()) return;
req("POST", "/webhook/teste-conectividade/" + URLEncoder.encode(tipo, StandardCharsets.UTF_8),
null, null, null, false);
}
static void removerWebhook() {
String tipo = prompt(" Tipo do webhook a remover (ex: pe.pedido.confirmado): ").trim();
if (tipo.isEmpty()) return;
req("DELETE", "/webhook/" + URLEncoder.encode(tipo, StandardCharsets.UTF_8),
null, null, null, false);
}
static void cadastrarPlaca() {
String placa = prompt(" Placa (ex: ABC1D23): ").trim().toUpperCase();
String di = promptOpt(" Data início monitoramento ISO 8601 (Enter p/ pular): ");
String df = promptOpt(" Data fim monitoramento ISO 8601 (Enter p/ pular): ");
StringBuilder body = new StringBuilder("{\"placa\":\"").append(escape(placa)).append("\"");
if (di != null) body.append(",\"dataInicioMonitoramento\":\"").append(escape(di)).append("\"");
if (df != null) body.append(",\"dataFimMonitoramento\":\"").append(escape(df)).append("\"");
body.append("}");
req("POST", "/placas", body.toString(), null, null, false);
}
static void removerPlaca() {
String placa = prompt(" Placa a remover (ex: ABC1D23): ").trim().toUpperCase();
req("DELETE", "/placas/" + placa);
}
static void listarTransacoes() {
Map<String,String> q = new LinkedHashMap<>();
q.put("placa", promptOpt(" Filtrar por placa (Enter p/ pular): "));
q.put("status", promptOpt(" Status [PENDENTE/PAGO/CANCELADA] (Enter p/ pular): "));
q.put("dataInicio", promptOpt(" Data início ISO 8601 (Enter p/ pular): "));
q.put("dataFim", promptOpt(" Data fim ISO 8601 (Enter p/ pular): "));
String pagina = promptOpt(" Página (Enter = 0): "); if (pagina == null) pagina = "0";
String tamanho = promptOpt(" Tamanho página (Enter = 50): "); if (tamanho == null) tamanho = "50";
q.put("pagina", pagina);
q.put("tamanhoPagina", tamanho);
q.values().removeIf(Objects::isNull);
req("GET", "/transacoes", null, q, null, false);
}
static void criarPedido() {
String raw = prompt(" IDs de transações separados por vírgula: ").trim();
StringBuilder transacoes = new StringBuilder("[");
boolean first = true;
for (String s : raw.split(",")) {
String id = s.trim();
if (id.isEmpty()) continue;
if (!first) transacoes.append(',');
transacoes.append("{\"transacaoId\":\"").append(escape(id)).append("\"}");
first = false;
}
transacoes.append("]");
String idemp = promptOpt(" Chave de idempotência (Opcional): ");
Map<String,String> headers = idemp != null ? Map.of("x-chave-idempotencia", idemp) : null;
req("POST", "/pedidos", "{\"transacoes\":" + transacoes + "}", null, headers, false);
}
static void consultarPedido() {
String pid = prompt(" ID do pedido: ").trim();
req("GET", "/pedidos/" + pid);
}
static void confirmarPagamento() {
String pid = prompt(" ID do pedido: ").trim();
String ref = promptOpt(" Referência externa (Enter p/ pular): ");
String dt = promptOpt(" Data liquidação externa ISO 8601 (Enter p/ pular): ");
String idemp = promptOpt(" Chave de idempotência (Opcional): ");
StringBuilder body = new StringBuilder("{");
boolean first = true;
if (ref != null) {
body.append("\"referenciaExterna\":\"").append(escape(ref)).append("\"");
first = false;
}
if (dt != null) {
if (!first) body.append(',');
body.append("\"dataLiquidacaoExterna\":\"").append(escape(dt)).append("\"");
}
body.append("}");
Map<String,String> headers = idemp != null ? Map.of("x-chave-idempotencia", idemp) : null;
String b = body.length() > 2 ? body.toString() : null;
req("POST", "/pedidos/" + pid + "/confirmar", b, null, headers, false);
}
static void cancelarPedido() {
String pid = prompt(" ID do pedido: ").trim();
String motivo = promptOpt(" Motivo [DESISTENCIA_PARCEIRO/ERRO_INTERNO/OUTROS] (Enter p/ pular): ");
String obs = promptOpt(" Observação até 500 chars (Enter p/ pular): ");
String idemp = promptOpt(" Chave de idempotência (Opcional): ");
StringBuilder body = new StringBuilder("{");
boolean first = true;
if (motivo != null) {
body.append("\"motivo\":\"").append(escape(motivo.toUpperCase())).append("\"");
first = false;
}
if (obs != null) {
if (!first) body.append(',');
body.append("\"observacao\":\"").append(escape(obs)).append("\"");
}
body.append("}");
Map<String,String> headers = idemp != null ? Map.of("x-chave-idempotencia", idemp) : null;
String b = body.length() > 2 ? body.toString() : null;
req("POST", "/pedidos/" + pid + "/cancelar", b, null, headers, false);
}
// ─── Menu ─────────────────────────────────────────────────────────────────
static class Item {
final String label;
final Runnable action;
Item(String l, Runnable a) { label = l; action = a; }
}
static List<Item> menu() {
return List.of(
new Item("── Primeiros Passos ─────────────────", null),
new Item("Descobrir parceiroId (GET /me)", Simulador::descobrirParceiro),
new Item("── Webhooks Config ──────────────────", null),
new Item("Listar eventos disponíveis", () -> listarEventosDisponiveis()),
new Item("Cadastrar webhook", Simulador::cadastrarWebhook),
new Item("Listar webhooks", Simulador::listarWebhooks),
new Item("Testar conectividade webhook", Simulador::testarConectividadeWebhook),
new Item("Remover webhook", Simulador::removerWebhook),
new Item("── Placas ───────────────────────────", null),
new Item("Cadastrar placa", Simulador::cadastrarPlaca),
new Item("Remover placa", Simulador::removerPlaca),
new Item("── Transações ───────────────────────", null),
new Item("Listar transações", Simulador::listarTransacoes),
new Item("── Pedidos ──────────────────────────", null),
new Item("Criar pedido", Simulador::criarPedido),
new Item("Consultar pedido", Simulador::consultarPedido),
new Item("Confirmar pagamento", Simulador::confirmarPagamento),
new Item("Cancelar pedido", Simulador::cancelarPedido)
);
}
static String padRight(String s, int n) {
return s.length() >= n ? s : s + " ".repeat(n - s.length());
}
static void printMenu() {
System.out.println("\n╔══════════════════════════════════════════════╗");
System.out.println("║ Movvia Arrecada+ — PE API Tester ║");
String shortUrl = baseUrl.replace("https://", "");
if (shortUrl.length() > 43) shortUrl = shortUrl.substring(0, 40) + "...";
System.out.println("║ " + padRight(shortUrl, 43) + "║");
String pid = cachedParceiroId != null
? "parceiroId: " + cachedParceiroId
: "parceiroId: (auto via GET /me)";
System.out.println("║ " + padRight(pid, 43) + "║");
System.out.println("╠══════════════════════════════════════════════╣");
int idx = 1;
for (Item it : menu()) {
if (it.action == null) {
System.out.println("║ " + padRight(it.label, 44) + "║");
} else {
System.out.println("║ " + padRight(String.format("%2d. %s", idx, it.label), 43) + "║");
idx++;
}
}
System.out.println("║ ║");
System.out.println("║ 0. Sair ║");
System.out.println("╚══════════════════════════════════════════════╝");
}
public static void main(String[] args) {
loadEnv();
baseUrl = envOr("BASE_URL", "https://hml.api.pedagioeletronico.com.br/gestao-webhooks-api/v1");
cachedParceiroId = env("PE_PARCEIRO_ID");
if (cachedParceiroId == null) {
System.out.println("\n ℹ️ PE_PARCEIRO_ID não configurado — chamando GET /me para descobrir...");
resolveParceiroId();
}
List<Runnable> actions = new ArrayList<>();
for (Item it : menu()) if (it.action != null) actions.add(it.action);
while (true) {
printMenu();
String choice = prompt("\nEscolha uma opção: ").trim().toLowerCase();
if (choice.equals("0") || choice.equals("sair")) break;
int n;
try { n = Integer.parseInt(choice); }
catch (NumberFormatException e) { System.out.println("Opção inválida."); continue; }
if (n < 1 || n > actions.size()) { System.out.println("Opção inválida."); continue; }
try { actions.get(n - 1).run(); }
catch (Exception e) { System.out.println("Erro inesperado: " + e.getMessage()); }
prompt("\n[Enter para voltar ao menu]");
}
System.out.println("\nAté logo!\n");
}
}#!/usr/bin/env bash
# Utilitário de teste para API Movvia Arrecada+ (PE)
# Requer: bash 4+, curl, jq
set -o pipefail
# ─── Pré-requisitos ───────────────────────────────────────────────────────────
command -v curl >/dev/null 2>&1 || {
echo "❌ curl não encontrado. Instale curl e rode novamente."
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo "❌ jq não encontrado. Instale jq (brew install jq / apt install jq) e rode novamente."
exit 1
}
# ─── Carregar .env ────────────────────────────────────────────────────────────
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$DIR/.env" ]; then
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
''|\#*) continue ;;
*=*)
key="${line%%=*}"
val="${line#*=}"
key="$(echo "$key" | tr -d '[:space:]')"
val="${val#\"}"; val="${val%\"}"
val="${val#\'}"; val="${val%\'}"
[ -n "$key" ] && [ -z "${!key:-}" ] && export "$key=$val"
;;
esac
done < "$DIR/.env"
fi
BASE_URL="${BASE_URL:-https://hml.api.pedagioeletronico.com.br/gestao-webhooks-api/v1}"
CACHED_PARCEIRO_ID="${PE_PARCEIRO_ID:-}"
check_creds() {
if [ -z "${PE_USERNAME:-}" ] || [ -z "${PE_PASSWORD:-}" ]; then
echo
echo "❌ Configure PE_USERNAME e PE_PASSWORD no arquivo .env"
echo " Copie .env.example → .env e preencha as credenciais."
echo
exit 1
fi
}
auth_header() {
printf 'Basic %s' "$(printf '%s:%s' "$PE_USERNAME" "$PE_PASSWORD" | base64 | tr -d '\n')"
}
uri_encode() {
jq -rn --arg s "$1" '$s|@uri'
}
# ─── Resolução de parceiroId ──────────────────────────────────────────────────
resolve_parceiro_id() {
if [ -n "$CACHED_PARCEIRO_ID" ]; then
printf '%s' "$CACHED_PARCEIRO_ID"
return
fi
local body status
status=$(curl -sS -o /tmp/movvia_me_body -w '%{http_code}' \
-H "Authorization: $(auth_header)" \
-H "Content-Type: application/json" \
"$BASE_URL/me" 2>/tmp/movvia_me_err) || {
echo "❌ Erro de rede em GET /me: $(cat /tmp/movvia_me_err)" >&2
exit 1
}
if [ "$status" -ge 400 ]; then
echo "❌ Não foi possível descobrir o parceiroId via GET /me ($status)" >&2
cat /tmp/movvia_me_body >&2
exit 1
fi
body=$(cat /tmp/movvia_me_body)
local pid
pid=$(echo "$body" | jq -r '.data.parceiroId // empty')
if [ -z "$pid" ]; then
echo "❌ Resposta sem data.parceiroId: $body" >&2
exit 1
fi
CACHED_PARCEIRO_ID="$pid"
echo " ℹ️ parceiroId descoberto via GET /me: $CACHED_PARCEIRO_ID" >&2
printf '%s' "$pid"
}
# ─── Função abstratora ────────────────────────────────────────────────────────
# req METHOD PATH [BODY] [QUERY_STRING] [EXTRA_HEADER...]
req() {
local method="$1" path="$2" body="${3:-}" query="${4:-}"
if [ "$#" -ge 4 ]; then shift 4; else shift "$#"; fi
local url="$BASE_URL$path"
[ -n "$query" ] && url="$url?$query"
local -a headers
headers=(-H "Authorization: $(auth_header)" -H "Content-Type: application/json")
if [ "$path" != "/me" ]; then
local pid
pid=$(resolve_parceiro_id)
headers+=(-H "x-parceiro-id: $pid")
fi
while [ "$#" -gt 0 ]; do
headers+=(-H "$1")
shift
done
echo
echo "→ $method $url"
[ -n "$body" ] && echo " Body: $body"
local -a curl_args
curl_args=(-sS -o /tmp/movvia_resp_body -w '%{http_code}' -X "$method" "$url" "${headers[@]}")
[ -n "$body" ] && curl_args+=(-d "$body")
local status
status=$(curl "${curl_args[@]}" 2>/tmp/movvia_resp_err) || {
echo "❌ Erro de rede: $(cat /tmp/movvia_resp_err)"
return 1
}
local mark="❌"
[ "$status" -lt 400 ] && mark="✅"
echo "$mark $status"
if [ -s /tmp/movvia_resp_body ]; then
if jq -e . /tmp/movvia_resp_body >/dev/null 2>&1; then
jq . /tmp/movvia_resp_body
else
cat /tmp/movvia_resp_body
fi
fi
}
# ─── Prompts ──────────────────────────────────────────────────────────────────
ask() {
local q="$1" v
read -r -p "$q" v
printf '%s' "$v"
}
# ─── Ações ────────────────────────────────────────────────────────────────────
EVENTOS_STATIC=(
"pe.pedido.criado"
"pe.pedido.confirmado"
"pe.pedido.cancelado"
"pe.pedido.expirado"
"pe.pedido.erro"
"pe.transacao.recebida"
"pe.transacao.atualizada"
"pe.transacao.cancelada"
)
descobrir_parceiro() { req GET "/me"; }
listar_eventos_disponiveis() { req GET "/webhook/eventos"; }
listar_webhooks() { req GET "/webhook"; }
cadastrar_webhook() {
local url chave
url=$(ask " URL HTTPS do endpoint: ")
chave=$(ask " Chave secreta HMAC: ")
echo
echo " Buscando eventos disponíveis..."
local body status
local pid
pid=$(resolve_parceiro_id)
body=$(curl -sS \
-H "Authorization: $(auth_header)" \
-H "x-parceiro-id: $pid" \
"$BASE_URL/webhook/eventos" 2>/dev/null) || body=""
local -a eventos
if [ -n "$body" ]; then
mapfile -t eventos < <(echo "$body" | jq -r '
.data | if type == "array" then
.[] | (if type == "string" then . else .evento end)
else empty end' 2>/dev/null)
fi
if [ "${#eventos[@]}" -eq 0 ]; then
eventos=("${EVENTOS_STATIC[@]}")
fi
echo
echo " Eventos disponíveis:"
local i=1
for e in "${eventos[@]}"; do
printf " %d. %s\n" "$i" "$e"
i=$((i + 1))
done
echo
local raw tipo
raw=$(ask " Escolha o número do evento ou digite o nome: ")
if [[ "$raw" =~ ^[0-9]+$ ]] && [ "$raw" -ge 1 ] && [ "$raw" -le "${#eventos[@]}" ]; then
tipo="${eventos[$((raw - 1))]}"
else
tipo="$raw"
fi
if [ -z "$tipo" ]; then
echo "❌ Tipo inválido."
return
fi
local body_json
body_json=$(jq -nc --arg url "$url" --arg c "$chave" --arg t "$tipo" \
'{url:$url, chaveSecreta:$c, tipo:$t}')
req POST "/webhook" "$body_json"
}
testar_conectividade_webhook() {
local tipo enc
tipo=$(ask " Tipo do evento (ex: pe.pedido.confirmado): ")
[ -z "$tipo" ] && return
enc=$(uri_encode "$tipo")
req POST "/webhook/teste-conectividade/$enc"
}
remover_webhook() {
local tipo enc
tipo=$(ask " Tipo do webhook a remover (ex: pe.pedido.confirmado): ")
[ -z "$tipo" ] && return
enc=$(uri_encode "$tipo")
req DELETE "/webhook/$enc"
}
cadastrar_placa() {
local placa di df body
placa=$(ask " Placa (ex: ABC1D23): ")
placa=$(echo "$placa" | tr '[:lower:]' '[:upper:]')
di=$(ask " Data início monitoramento ISO 8601 (Enter p/ pular): ")
df=$(ask " Data fim monitoramento ISO 8601 (Enter p/ pular): ")
body=$(jq -nc --arg p "$placa" --arg di "$di" --arg df "$df" '
{placa:$p}
+ (if $di != "" then {dataInicioMonitoramento:$di} else {} end)
+ (if $df != "" then {dataFimMonitoramento:$df} else {} end)')
req POST "/placas" "$body"
}
remover_placa() {
local placa
placa=$(ask " Placa a remover (ex: ABC1D23): ")
placa=$(echo "$placa" | tr '[:lower:]' '[:upper:]')
req DELETE "/placas/$placa"
}
build_query() {
local out=""
while [ "$#" -gt 0 ]; do
local k="$1" v="$2"
shift 2
[ -z "$v" ] && continue
local enc_k enc_v
enc_k=$(uri_encode "$k")
enc_v=$(uri_encode "$v")
[ -n "$out" ] && out="$out&"
out="$out$enc_k=$enc_v"
done
printf '%s' "$out"
}
listar_transacoes() {
local placa status di df pagina tamanho q
placa=$(ask " Filtrar por placa (Enter p/ pular): ")
status=$(ask " Status [PENDENTE/PAGO/CANCELADA] (Enter p/ pular): ")
di=$(ask " Data início ISO 8601 (Enter p/ pular): ")
df=$(ask " Data fim ISO 8601 (Enter p/ pular): ")
pagina=$(ask " Página (Enter = 0): "); pagina="${pagina:-0}"
tamanho=$(ask " Tamanho página (Enter = 50): "); tamanho="${tamanho:-50}"
q=$(build_query placa "$placa" status "$status" \
dataInicio "$di" dataFim "$df" \
pagina "$pagina" tamanhoPagina "$tamanho")
req GET "/transacoes" "" "$q"
}
criar_pedido() {
local raw idemp body
raw=$(ask " IDs de transações separados por vírgula: ")
body=$(jq -nc --arg s "$raw" '{
transacoes: ($s | split(",")
| map(. | gsub("^\\s+|\\s+$"; ""))
| map(select(length > 0))
| map({transacaoId: .}))
}')
idemp=$(ask " Chave de idempotência (Opcional): ")
if [ -n "$idemp" ]; then
req POST "/pedidos" "$body" "" "x-chave-idempotencia: $idemp"
else
req POST "/pedidos" "$body"
fi
}
consultar_pedido() {
local pid
pid=$(ask " ID do pedido: ")
req GET "/pedidos/$pid"
}
confirmar_pagamento() {
local pid ref dt idemp body
pid=$(ask " ID do pedido: ")
ref=$(ask " Referência externa (Enter p/ pular): ")
dt=$(ask " Data liquidação externa ISO 8601 (Enter p/ pular): ")
idemp=$(ask " Chave de idempotência (Opcional): ")
body=$(jq -nc --arg ref "$ref" --arg dt "$dt" '
(if $ref != "" then {referenciaExterna:$ref} else {} end)
+ (if $dt != "" then {dataLiquidacaoExterna:$dt} else {} end)')
[ "$body" = "{}" ] && body=""
if [ -n "$idemp" ]; then
req POST "/pedidos/$pid/confirmar" "$body" "" "x-chave-idempotencia: $idemp"
else
req POST "/pedidos/$pid/confirmar" "$body"
fi
}
cancelar_pedido() {
local pid motivo obs idemp body
pid=$(ask " ID do pedido: ")
motivo=$(ask " Motivo [DESISTENCIA_PARCEIRO/ERRO_INTERNO/OUTROS] (Enter p/ pular): ")
obs=$(ask " Observação até 500 chars (Enter p/ pular): ")
idemp=$(ask " Chave de idempotência (Opcional): ")
if [ -n "$motivo" ]; then
motivo=$(echo "$motivo" | tr '[:lower:]' '[:upper:]')
fi
body=$(jq -nc --arg m "$motivo" --arg o "$obs" '
(if $m != "" then {motivo:$m} else {} end)
+ (if $o != "" then {observacao:$o} else {} end)')
[ "$body" = "{}" ] && body=""
if [ -n "$idemp" ]; then
req POST "/pedidos/$pid/cancelar" "$body" "" "x-chave-idempotencia: $idemp"
else
req POST "/pedidos/$pid/cancelar" "$body"
fi
}
# ─── Menu ─────────────────────────────────────────────────────────────────────
MENU_LABELS=(
"── Primeiros Passos ─────────────────"
"Descobrir parceiroId (GET /me)"
"── Webhooks Config ──────────────────"
"Listar eventos disponíveis"
"Cadastrar webhook"
"Listar webhooks"
"Testar conectividade webhook"
"Remover webhook"
"── Placas ───────────────────────────"
"Cadastrar placa"
"Remover placa"
"── Transações ───────────────────────"
"Listar transações"
"── Pedidos ──────────────────────────"
"Criar pedido"
"Consultar pedido"
"Confirmar pagamento"
"Cancelar pedido"
)
MENU_ACTIONS=(
""
"descobrir_parceiro"
""
"listar_eventos_disponiveis"
"cadastrar_webhook"
"listar_webhooks"
"testar_conectividade_webhook"
"remover_webhook"
""
"cadastrar_placa"
"remover_placa"
""
"listar_transacoes"
""
"criar_pedido"
"consultar_pedido"
"confirmar_pagamento"
"cancelar_pedido"
)
print_menu() {
echo
echo "╔══════════════════════════════════════════════╗"
echo "║ Movvia Arrecada+ — PE API Tester ║"
local short="${BASE_URL#https://}"
if [ "${#short}" -gt 43 ]; then
short="${short:0:40}..."
fi
printf "║ %-43s║\n" "$short"
local pid
if [ -n "$CACHED_PARCEIRO_ID" ]; then
pid="parceiroId: $CACHED_PARCEIRO_ID"
else
pid="parceiroId: (auto via GET /me)"
fi
printf "║ %-43s║\n" "$pid"
echo "╠══════════════════════════════════════════════╣"
local idx=1
for i in "${!MENU_LABELS[@]}"; do
if [ -z "${MENU_ACTIONS[$i]}" ]; then
printf "║ %-44s║\n" "${MENU_LABELS[$i]}"
else
printf "║ %2d. %-39s║\n" "$idx" "${MENU_LABELS[$i]}"
idx=$((idx + 1))
fi
done
echo "║ ║"
echo "║ 0. Sair ║"
echo "╚══════════════════════════════════════════════╝"
}
main() {
check_creds
if [ -z "$CACHED_PARCEIRO_ID" ]; then
echo
echo " ℹ️ PE_PARCEIRO_ID não configurado — chamando GET /me para descobrir..."
CACHED_PARCEIRO_ID=$(resolve_parceiro_id)
fi
local -a active=()
for a in "${MENU_ACTIONS[@]}"; do
[ -n "$a" ] && active+=("$a")
done
while true; do
print_menu
echo
local choice
read -r -p "Escolha uma opção: " choice
choice="${choice,,}"
if [ "$choice" = "0" ] || [ "$choice" = "sair" ]; then
break
fi
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
echo "Opção inválida."
continue
fi
if [ "$choice" -lt 1 ] || [ "$choice" -gt "${#active[@]}" ]; then
echo "Opção inválida."
continue
fi
"${active[$((choice - 1))]}" || true
echo
read -r -p "[Enter para voltar ao menu] " _
done
echo
echo "Até logo!"
echo
}
mainCrie .env.example com o template de credenciais:
# Credenciais Basic Auth (fornecidas pela Movvia no onboarding)
PE_USERNAME=seu_usuario
PE_PASSWORD=sua_senha
# ID do Parceiro — OPCIONAL. Se omitido, o script chama GET /me automaticamente
# para descobrir o parceiroId vinculado às credenciais acima.
# PE_PARCEIRO_ID=prc_conectcar
# URL base — padrão é homologação (descomente para sobrescrever)
# BASE_URL=https://hml.api.pedagioeletronico.com.br/gestao-webhooks-api/v1E o start.sh, que pede usuário/senha na primeira run e despacha pra linguagem escolhida:
#!/bin/sh
# Bootstrap do simulador. Onboarding de credenciais na primeira run e despacho
# pra linguagem escolhida via --lang=node|python|go|java|bash (default node).
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR" || exit 1
SIM_LANG="node"
for arg in "$@"; do
case "$arg" in
--lang=*) SIM_LANG="${arg#--lang=}" ;;
-h|--help)
echo "Uso: $0 [--lang=node|python|go|java|bash]"
echo "Default: node"
exit 0
;;
esac
done
if [ ! -f .env ]; then
echo "Primeira execução — configurando credenciais."
printf "Usuário: "; read -r u
printf "Senha: "; stty -echo; read -r p; stty echo; echo
printf "PE_USERNAME=%s\nPE_PASSWORD=%s\n" "$u" "$p" > .env
echo "Credenciais salvas em .env."
fi
case "$SIM_LANG" in
node) node simulador.js ;;
python|py) python3 simulador.py ;;
go) go run simulador.go ;;
java) java Simulador.java ;;
bash|sh) bash simulador.sh ;;
*)
echo "Linguagem inválida: $SIM_LANG"
echo "Uso: $0 [--lang=node|python|go|java|bash]"
exit 1
;;
esacchmod +x start.sh
./start.sh # roda Node.js (default)
./start.sh --lang=python # Python
./start.sh --lang=go # Go
./start.sh --lang=java # Java
./start.sh --lang=bash # Bash + curl + jqNa primeira execução, o start.sh pergunta usuário e senha, salva no .env e abre o menu. As próximas execuções usam o .env direto.
Se preferir invocar a linguagem manualmente, sem o start.sh:
node simulador.jspython3 simulador.pygo run simulador.gojava Simulador.javabash simulador.sh| Grupo | Operações |
|---|---|
| Primeiros Passos | Descobrir parceiroId via GET /me (automático no startup) |
| Webhooks | Listar eventos, cadastrar, listar, testar conectividade, remover |
| Placas | Cadastrar, remover |
| Transações | Listar com filtros (paginação inicia em 0) |
| Pedidos | Criar, consultar, confirmar, cancelar |
Toda chamada HTTP é impressa no formato:
→ METHOD URL
Body: { ... }
✅ 200
{ "data": ... }╔══════════════════════════════════════════════╗
║ Movvia Arrecada+ — PE API Tester ║
║ hml.api.pedagioeletronico.com.br/... ║
║ parceiroId: par_7f3a1b2c ║
╠══════════════════════════════════════════════╣
║ ── Primeiros Passos ───────────────── ║
║ 1. Descobrir parceiroId (GET /me) ║
║ ── Webhooks Config ────────────────── ║
║ 2. Listar eventos disponíveis ║
║ 3. Cadastrar webhook ║
║ 4. Listar webhooks ║
║ 5. Testar conectividade webhook ║
║ 6. Remover webhook ║
║ ── Placas ─────────────────────────── ║
║ 7. Cadastrar placa ║
║ 8. Remover placa ║
║ ── Transações ─────────────────────── ║
║ 9. Listar transações ║
║ ── Pedidos ────────────────────────── ║
║ 10. Criar pedido ║
║ 11. Consultar pedido ║
║ 12. Confirmar pagamento ║
║ 13. Cancelar pedido ║
║ ║
║ 0. Sair ║
╚══════════════════════════════════════════════╝- Validar HMAC-SHA256 — verifique a assinatura dos webhooks que vão chegar.
- Confirmar um pedido — fluxo completo de criação e confirmação.
- Referência da API — todos os endpoints e schemas.