Pular para o conteúdo

Omie Sync State — Bubble Data Type

Omie Sync State é a projeção read-only no Bubble do estado da integração Omie. Modelo de dados completo do tipo que o backend mantém e Bubble apenas consome.

Cardinalidade: 1 Omie Sync State ↔ 1 Order (constraint: order field unique). Cria no primeiro POST /sync-omie. Dura toda vida do Order.

Order é dono dos dados comerciais (Bubble nativo); Omie Sync State carrega o estado técnico-fiscal escrito pelo backend. Separar evita churn de modified_date no Order e simplifica privacy rules.

CritérioOrder (Bubble nativo)Omie Sync State (novo)
OwnerComercial · clienteBackend orquestrador
Frequência updateBaixa (criação + cancel)Alta (1 PATCH por mudança)
Fonte de verdadeBubble PostgresBackend Postgres → projetado pra Bubble
Escrita Bubble workflowSim (checkout, etc)Não — só backend
Bubble Privacy RuleVisibilidade comercialVisibilidade Financeiro + Operação
modified_at churnReflete ação humanaReflete sync técnico — separa sinal
Bind UI Repeating Group”Pedidos do cliente""Pedidos com erro” / “Bloqueados dia 25” / “Em cooldown”
CampoTipo BubbleDescriçãoConstraint
orderOrderLink 1:1 com Order paiUnique · obrigatório
bubble_order_idtextMirror do Order's unique_id (lookup rápido pelo backend)Unique
omie_companytextSempre "AABC" nesta versão (LNG fora do escopo)Default "AABC"
CampoTipo BubbleDescrição
status_pedidoOption Set Omie Status Pedido (3 vals)Dimensão comercial — espelha decisão Pagar.me
status_faturamentoOption Set Omie Status Faturamento (9 vals)Dimensão fiscal/Omie — onde regra dia 25 aparece
previous_status_faturamentoOption Set Omie Status FaturamentoEstado anterior — útil pra exibir transição na UI
CampoTipo BubbleDescrição
billing_triggerOption Set Omie Billing Trigger (immediate/pos_evento/custom_date)Como cliente quer disparar — config do Customer
billing_scheduled_datedateQuando ARQ vai acordar e faturar — único pra todos modos de agendamento
billing_reasontextBillingDecision.reason — “today > cutoff → roll to next month day 1”
regra_dia25_aplicadayes/noTrue quando BillingStatus = deferred_by_cutoff
regra_dia25_override_aplicadayes/noOperador editou data via /override-day25
regra_dia25_override_motivotextMotivo obrigatório do override (texto livre)
regra_dia25_override_byUserQuem clicou override (Bubble user link)
regra_dia25_override_atdateTimestamp do override
CampoTipo BubbleDescrição
omie_cliente_idnumbercodigo_cliente_omie — populado por IncluirCliente
omie_projeto_idnumbercProjeto na Omie — IncluirProjeto
omie_os_idnumbercChave do IncluirOS — required pra emitir NFS-e
omie_nfse_idnumbercChave da NFS-e — EmitirNFSe
numero_nfsetextNúmero formal NFS-e (XX/AAAA)
link_nfsetext (URL)URL pra PDF NFS-e na Prefeitura
tipo_servico_omietextCódigo serviço determinado por routing.py (cidade + órgão público)
CampoTipo BubbleDescrição
last_sync_atdateTimestamp do último PATCH pelo backend (every write)
last_sync_endpointtext"reconciler:cdc_tick" / "manual:sync-omie" / "IncluirCliente" / "FaturarOS" / "EmitirNFSe" / "ARQ:run_billing_job" — útil pra debug
last_sync_errortextMensagem humana do último erro · null se ok
last_sync_error_codetextClass name do exception (OmieValidationError / OmieBanned / OmieRetryExhausted)
sync_attempts_countnumberContador total de tentativas no estado atual (reseta ao mudar estado)
sync_correlation_idtextIdempotency-Key da última operação · pra cross-ref com backend action_log
CampoTipo BubbleDescrição
omie_ban_activeyes/noTrue quando BanCooldown.is_banned · libera auto ao expirar
omie_ban_started_atdateQuando bateu 425 (do banco backend)
omie_ban_expires_atdatestarted_at + 1830s · countdown na UI
omie_ban_triggered_by_endpointtextQual call disparou (ex: "IncluirOS")

Nove estados, transições disparadas por: reconciler detectando delta no Order Bubble (cron 1h ou manual /sync-omie) + ARQ scheduler run_billing_job + respostas Omie.

stateDiagram-v2
    direction LR
    [*] --> aguardando_pagamento : Omie Sync State criado (POST /sync-omie)
    aguardando_pagamento --> aguardando_confirmacao : flag cliente requires_manual_confirmation
    aguardando_pagamento --> bloqueado_dia25 : reconciler detecta confirmado · today.day > 25 · PJ cutoff
    aguardando_pagamento --> agendado : reconciler detecta confirmado · trigger = pos_evento
    aguardando_pagamento --> faturando : reconciler detecta confirmado · imediato
    aguardando_confirmacao --> bloqueado_dia25 : libera + day>25
    aguardando_confirmacao --> faturando : libera + day<=25
    bloqueado_dia25 --> faturando : ARQ acorda · scheduled_date
    bloqueado_dia25 --> bloqueado_dia25 : operador override (nova data)
    agendado --> faturando : ARQ acorda · event_date+1
    faturando --> nf_emitida : Omie OK
    faturando --> erro_faturamento : Omie 4xx/5xx/timeout
    erro_faturamento --> faturando : reconciler retenta (próximo tick) ou manual /sync-omie
    erro_faturamento --> faturamento_manual : reconciler detecta Order.faturamento_manual=yes
    nf_emitida --> nf_cancelada : reconciler detecta Order.status_pedido=cancelado
    nf_emitida --> [*]
    nf_cancelada --> faturando : reemissão manual
    nf_cancelada --> [*]
    faturamento_manual --> [*]

Omie Sync State é sempre escrito de dentro do reconciler — nunca em resposta a webhook Bubble→backend. Dois únicos pontos de chamada:

  1. Cron cdc_tick (a cada 1h) — loop por order modificado desde o cursor; chama _reconcile_one(order) que, ao final, chama project_omie_sync_state(...).
  2. Endpoint manual POST /v1/orders/{id}/sync-omie — chama o mesmo _reconcile_one(order) direto, sem mexer no cursor.

Ambos passam pelo helper único project_omie_sync_state. Não há outro caminho — endpoints /retry, /cancel-nf, /mark-nf-manual, /override-day25, /payment-confirmed foram removidos; suas semânticas foram consolidadas no reconciler (ver Integration).

PrincípioImplementação
Source of truthBackend Postgres (order_sync_state + order_shadow + bubble_sync_cursor). Bubble = projeção.
Frequência1 PATCH por transição detectada no diff shadow → fresh (noop quando nada relevante mudou)
IdempotênciaBackend usa bubble_order_id pra resolver o Omie Sync State do Bubble (busca via Data API Search). correlation_id derivado de cdc:{order_id}:{order.modified_date}
CriaçãoPrimeira aparição do order no reconciler → POST /api/1.1/obj/omiesyncstate
Falha Bubble Data APINão avança cursor naquele order → próximo tick reprocessa o range
Ordem dos camposSempre escreve last_sync_at + last_sync_endpoint + sync_correlation_id + os campos transicionais. Outros campos não tocados = não enviados.
# backend/src/blu_omie/services/sync_state_projection.py (Slice F)
# Chamado por: reconciler tick OU handler manual /sync-omie.
# Nunca chamado direto por webhook Bubble.
async def project_omie_sync_state(
*,
bubble_order_id: str,
state_delta: dict, # só campos que mudaram
correlation_id: str, # cdc:{order_id}:{modified_date} | manual:{uuid}
source_endpoint: str, # "reconciler:cdc_tick" | "manual:sync-omie" | "ARQ:run_billing_job"
) -> None:
syncstate = await bubble_search(
type="omiesyncstate",
constraints=[{"key": "bubble_order_id", "constraint_type": "equals",
"value": bubble_order_id}],
)
payload = {
**state_delta,
"last_sync_at": now().isoformat(),
"last_sync_endpoint": source_endpoint,
"sync_correlation_id": correlation_id,
}
if syncstate is None:
# First sighting — create
await bubble.post(type="omiesyncstate",
body={"bubble_order_id": bubble_order_id,
"order": bubble_order_id,
"omie_company": "AABC",
**payload})
else:
await bubble.patch(type="omiesyncstate",
id=syncstate["_id"],
body=payload)
PatternBubble expression
Achar Omie Sync State de 1 OrderSearch for Omie Sync States :first item · constraint: order = Current Page Order
Repeating Group “Bloqueados dia 25”Search for Omie Sync States :filter status_faturamento is bloqueado_dia25
Repeating Group “Erros pendentes”Search for Omie Sync States :filter status_faturamento is erro_faturamento
Repeating Group “Em cooldown”Search for Omie Sync States :filter omie_ban_active is yes
KPI “NF emitidas hoje”Search for Omie Sync States :count · constraints: status_faturamento=nf_emitida AND last_sync_at >= today
Badge dinâmicoCurrent Omie Sync State's status_faturamento's Display + cor da Option Set
RoleAcessoJustificativa
FinanceiroVer tudo · ações backoffice (override, retry, cancel-nf)Donos do faturamento
OperaçãoVer tudo · sem ações de cancel/overrideAcompanhamento operacional
ComercialVer status_pedido + status_faturamento (sem detalhes técnicos de erro/ban)Conhecer estado pro cliente, sem ruído técnico
Cliente (Website)Sem acessoOmie Sync State é metadado interno. Cliente vê só status_pedido no Order direto.
Backend service accountR/W completo via API keyÚnico escritor

Três Option Sets, criados antes de adicionar os campos no Data Type. Cores em hex usadas em attr_cor para binding direto na UI.

Omie Status Pedido (3 vals):

slugdisplayattr_cor
aguardando_pagamento”Aguardando pagamento”#fde047 (amarelo)
confirmado”Confirmado”#6ee7b7 (verde)
cancelado”Cancelado”#52525b (cinza escuro)

Omie Status Faturamento (9 vals):

slugdisplayattr_cor
aguardando_pagamento”Aguardando pagamento”#fde047
aguardando_confirmacao”Aguardando confirmação”#fb923c (laranja)
bloqueado_dia25”Bloqueado dia 25”#ef4444 (vermelho)
agendado”Agendado”#7dd3fc (azul claro)
faturando”Faturando…”#3b82f6 (azul)
nf_emitida”NF Emitida”#10b981
nf_cancelada”NF Cancelada”#737373
erro_faturamento”Erro de faturamento”#991b1b (vermelho escuro)
faturamento_manual”Faturamento manual”#d4d4d4 (cinza claro)

Omie Billing Trigger (3 vals):

slugdisplay
immediate”Imediato”
pos_evento”Pós-evento”
custom_date”Data customizada”

Mapeamento Bubble Element → bind expression → comportamento. Cada linha é 1 widget concreto na tela de detalhe do pedido ou no Console Financeiro.

Elemento BubbleBind / CondicionalComportamento
Badge “Status Faturamento”Current Omie Sync State's status_faturamento's display + attr_corCor automática via Option Set
Alerta topo “Bloqueado dia 25”regra_dia25_aplicada is yes”Postergado para [billing_scheduled_date]” + link “Editar data”
Banner “Omie em cooldown”omie_ban_active is yes”Omie bloqueou — libera em [omie_ban_expires_at - Current date and time] minutos”
Botão “Forçar sync agora”status_faturamento is erro_faturamento AND omie_ban_active is noChama POST /v1/orders/{id}/sync-omie (mesmo handler do reconciler, parametrizado pra 1 order)
Botão “Override dia 25”status_faturamento is bloqueado_dia25 AND Current User's role is FinanceiroEdita Order.billing_scheduled_date_override + Order.regra_dia25_override_motivo no Bubble. Reconciler propaga no próximo tick (ou clica “Forçar sync” pra imediato).
Card “Sincronização Omie” (4 colunas)Group com 4 sub-grupos: cliente / projeto / OS / NFS-eCada um: check se omie_*_id is not empty, spinner se aguarda, x se last_sync_error_code mapeia
Link “Ver NF”link_nfse is not emptyExternal link em nova aba
”Última sincronização”last_sync_at :formatted as "HH:MM dd/mm"Discreto no rodapé do detalhe
Tooltip técnico (debug financeiro)Hover: last_sync_endpoint + last_sync_error_code + sync_correlation_idPra abrir ticket suporte com correlation_id

Cada delta detectado pelo reconciler (ou ação dentro da sequência Omie) gera um PATCH específico. Útil pra debugar “quem escreveu o quê e quando” via last_sync_endpoint + sync_correlation_id.

Trigger backendCampos escritos no PATCH
Reconciler 1ª aparição do orderCria registro. bubble_order_id, order, omie_company, status_pedido, status_faturamento=aguardando_pagamento, billing_trigger, tipo_servico_omie. last_sync_endpoint="reconciler:cdc_tick" (ou "manual:sync-omie").
Reconciler detecta delta.status_pedido = confirmadodecide_billingstatus_pedido=confirmado, status_faturamento = faturando | bloqueado_dia25 | agendado | aguardando_confirmacao, billing_scheduled_date, billing_reason, regra_dia25_aplicada
Reconciler detecta delta.status_pedido = canceladostatus_pedido=cancelado. Se NF emitida: dispara CancelarNFSestatus_faturamento=nf_cancelada. Se pré-NF: status_faturamento mantido + OS cancelada na Omie.
Reconciler detecta delta.billing_scheduled_date_override (operador editou no Bubble)billing_scheduled_date (novo), regra_dia25_override_aplicada=yes, regra_dia25_override_motivo, regra_dia25_override_by, regra_dia25_override_at. Reagenda run_billing_job.
Reconciler detecta delta.faturamento_manual = yes (operador marcou no Bubble)status_faturamento=faturamento_manual, numero_nfse, omie_nfse_id (vindos do Order).
IncluirCliente resp OK (dentro do reconciler)omie_cliente_id, last_sync_endpoint="IncluirCliente"
IncluirProjeto resp OKomie_projeto_id
IncluirOS resp OKomie_os_id
ARQ scheduler run_billing_job acordou (deferido por dia 25 / agendado)status_faturamento=faturando, last_sync_endpoint="ARQ:run_billing_job"
EmitirNFSe resp OKstatus_faturamento=nf_emitida, omie_nfse_id, numero_nfse, link_nfse
Omie call retornou erro retryablesync_attempts_count++, last_sync_error, last_sync_error_code (status NÃO muda) — próximo tick reconciler retenta.
Omie call retornou OmieRetryExhaustedstatus_faturamento=erro_faturamento, last_sync_error, last_sync_error_code
BanCooldown.record_ban (HTTP 425)omie_ban_active=yes, omie_ban_started_at, omie_ban_expires_at, omie_ban_triggered_by_endpoint, status_faturamento=erro_faturamento
BanCooldown expira (lazy check no próximo tick)omie_ban_active=no (timestamps mantidos pra histórico)
Manual POST /v1/orders/{id}/sync-omieMesmo handler do tick, parametrizado para 1 order. last_sync_endpoint="manual:sync-omie". Sem mexer no cursor.