Pular para o conteúdo

Testability — /v1/test/* endpoints

Sete endpoints exclusivos pra sandbox/teste. Gated por env ENABLE_TEST_API=YES. Em prod (env ≠ YES) todos retornam 404. Mesma auth Authorization: Bearer que endpoints regulares.

  1. Idempotência total — chamar 2× == 1×, sem estado parasita.
  2. Sem hit Omie real por defaultseed-order cria registro em DB local + Bubble Data API; não cria nada na Omie até sync-omie ser chamado depois.
  3. Trial-only operationsreset scope trial-data clica botão Omie via headless. Recusa 403 se OMIE_APP_KEY for produtivo.
  4. Structured logs — todo endpoint emite event test.{action}.{result} via structlog.
  5. Bubble-friendly responses — JSON flat, sem nested >2 níveis, campos snake_case.

Sucesso:

{
"ok": true,
"data": { ... },
"elapsed_ms": 42,
"test_request_id": "tr-1779466047-abc123"
}

Erro:

{
"ok": false,
"error": { "code": "FORBIDDEN_IN_PROD", "message": "..." },
"elapsed_ms": 3,
"test_request_id": "tr-..."
}

Cria order de teste em Bubble + records auxiliares no DB local. Templates pré-definidos cobrem cenários A1–A6 + stress.

IDCenárioNotes
happy-pjCliente PJ, evento SP, valor 1500A1 base
happy-pfCliente PF, evento RJA2 base
cancel-flowPJ + flag expected_cancel: trueA3/A6 setup
ban-triggerCria 5 orders consecutivos, força throttlestress
city-no-homologationCidade sem NFS-e (ex: pequena interior)A4 fail-mode
large-qty100 participantesedge case
POST /v1/test/seed-order
Authorization: Bearer <token>
Content-Type: application/json
{
"template": "happy-pj",
"overrides": {
"valor": 2500,
"cidade": "São Paulo"
}
}
{
"ok": true,
"data": {
"bubble_order_id": "test-1779466047-a3f",
"omie_company": "AABC",
"template_used": "happy-pj",
"expected_status_after_sync": "synced",
"fields": {
"cliente_nome": "Teste PJ Ltda",
"cnpj": "12345678000195",
"evento_sigla": "TEST",
"valor": 2500,
"cidade": "São Paulo",
"data": "2026-06-15"
},
"bubble_data_api_url": "https://eventos.blueprintt.co/version-test/api/1.1/obj/order/test-1779466047-a3f"
},
"elapsed_ms": 412
}
  • 400 INVALID_TEMPLATE — template desconhecido
  • 503 BUBBLE_UNREACHABLE — Bubble Data API offline
  • 403 FORBIDDEN_IN_PROD — env gate

Força order em estado específico sem esperar workflow real completar. Útil pra validar branches de UI Bubble.

POST /v1/test/force-state
{
"bubble_order_id": "test-...",
"target": "synced" | "syncing" | "error" | "banned",
"reason_override": "manual test trigger"
}
  • synced — escreve action_log + omie_id_map fake + PATCH Bubble order.status="synced"
  • syncing — escreve in-flight record (não persiste após restart, simula transient)
  • error — registra action_log com error_reason + PATCH Bubble order.status="error"
  • banned — circuit_breaker força open + PATCH Bubble order.status="banned"
{
"ok": true,
"data": {
"bubble_order_id": "...",
"from_state": "pending",
"to_state": "error",
"actions_taken": ["wrote action_log", "patched bubble"]
}
}

Dispara jobs ARQ manualmente, sem esperar cron.

POST /v1/test/trigger-background
{
"job": "billing-dia25" | "cleanup-scheduler" | "park-retry" | "matrix-run",
"args": { ... job-specific ... }
}
  • billing-dia25 — força billing scheduler agora (normal: dia 25 do mês)
  • cleanup-scheduler — força action_log retention + idempotency_cache TTL cleanup
  • park-retry — força resume de jobs parked
  • matrix-run — dispara run_matrix_with_park para um scenario_id
{
"ok": true,
"data": {
"job_name": "billing-dia25",
"arq_job_id": "billing-dia25-1779466047",
"scheduled_for": "2026-05-22T11:30:00Z",
"queue_depth_after": 3
}
}

Replay ou sintetiza evento Omie inbound pra testar handlers downstream sem esperar evento real.

POST /v1/test/simulate-omie-webhook
{
"event": "nfse.autorizada" | "nfse.cancelada" | "os.faturada" | "os.cancelada",
"bubble_order_id": "test-...",
"payload_overrides": { ... }
}
  • Constrói payload Omie-shaped baseado em template do evento
  • Chama internamente o webhook handler como se viesse externo
  • Sem HTTP loop (chama função handler direto)
{
"ok": true,
"data": {
"event_simulated": "nfse.autorizada",
"handler_response": { ... },
"downstream_effects": ["patched bubble", "wrote invoice_ledger"]
}
}

Wipe state. Scopes selecionáveis. Útil entre runs de teste.

POST /v1/test/reset
{
"scope": "trial-data" | "idempotency-cache" | "action-log" | "all",
"dry_run": false
}
  • trial-data — headless clica “Limpar dados de demonstração” no Omie ERP. Recusa 403 se OMIE_APP_KEY for produtivo (verifica via meta endpoint).
  • idempotency-cache — DELETE FROM idempotency_cache no Postgres.
  • action-log — DELETE FROM action_log WHERE created_at < now() - 1h (mantém histórico recente).
  • all — todos acima.
{
"ok": true,
"data": {
"scope": "all",
"actions": [
{"name": "trial-data clear", "removed": 14, "via": "headless"},
{"name": "idempotency-cache flush", "removed": 47},
{"name": "action-log trim", "removed": 230}
]
},
"elapsed_ms": 8350
}
  • Endpoint nunca toca trial-data se OMIE_APP_KEY corresponder a app produtivo (whitelist em config).
  • dry_run: true — lista o que faria sem executar.

Simula chamada de qualquer endpoint sem hitar Omie. Tracing total. Sem custo de rate-limit.

POST /v1/test/dry-run
{
"endpoint": "/v1/orders/{id}/sync-omie",
"method": "POST",
"path_params": {"id": "test-..."},
"payload": { ... }
}
  • Resolve endpoint → executa internamente até a barreira “Omie call”
  • No lugar de chamar Omie, retorna o payload que seria enviado + a resposta esperada (template)
  • Útil pra Bubble dev verificar shape de payload sem custo
{
"ok": true,
"data": {
"endpoint": "/v1/orders/{id}/sync-omie",
"would_call_omie": [
{"call": "IncluirCliente", "param": {...}},
{"call": "IncluirProjeto", "param": {...}},
{"call": "IncluirOS", "param": {...}}
],
"would_return": { "status": "synced", "omie_ids": "..." },
"no_actual_state_change": true
}
}

Estado completo: trial banned + last-call timestamps + throttle counters + cache hits + queue depth. Tudo num único JSON.

GET /v1/test/health-deep
Authorization: Bearer <token>
{
"ok": true,
"data": {
"timestamp": "2026-05-22T11:42:30Z",
"components": {
"db": {"status": "ok", "latency_ms": 4},
"omie_clients": {
"AABC": {
"status": "throttled",
"last_call_at": "2026-05-22T11:42:28Z",
"next_call_eligible_at": "2026-05-22T11:42:30Z",
"circuit_state": "closed",
"consecutive_failures": 0
}
},
"bubble_data_api": {"status": "ok", "latency_ms": 132},
"idempotency_cache": {"entries": 47, "hit_rate_5m": 0.83},
"action_log": {"entries_last_1h": 230},
"arq_queue": {"depth": 0, "workers_alive": 1}
},
"trial_status": {
"is_blocked": false,
"ban_expires_at": null,
"last_redundant_at": "2026-05-22T11:30:00Z"
},
"version": {"sha": "8959ad0", "build_at": "2026-05-22T10:55Z"}
}
}
Bubble cron hourly → GET /v1/test/health-deep
→ if ok=false OR trial_status.is_blocked → alert PM via Bubble email
backend/src/blu_omie/test_api/__init__.py
from fastapi import APIRouter, Depends, HTTPException
router = APIRouter(prefix="/v1/test", tags=["testability"])
def require_test_api_enabled():
if os.environ.get("ENABLE_TEST_API") != "YES":
raise HTTPException(status_code=404)
return True
@router.post("/seed-order", dependencies=[Depends(require_test_api_enabled)])
def seed_order(...): ...

.env.example:

# Enable /v1/test/* sandbox endpoints (sandbox/staging only — NEVER in prod)
ENABLE_TEST_API=YES
[Bubble admin "Sandbox Console" page]
├── Button: Seed (template selector) → POST /v1/test/seed-order
├── Last seeded order_id: <text>
├── Button: Sync this → POST /v1/orders/{id}/sync-omie
├── Button: Verify → GET /v1/orders/{id}/status
├── Button: Force error → POST /v1/test/force-state
├── Button: Dry-run → POST /v1/test/dry-run
├── Button: Reset all → POST /v1/test/reset {scope: "all"}
└── Live JSON: GET /v1/test/health-deep (auto-refresh 30s)

Substitui “torcer para funcionar” por “controlar e ver”.

Ver também: Bubble Dev — Fases de integração (Fase 1 usa estes endpoints).