Skip to main content
Descontinuado (v0). Contrato anterior, mantido apenas como referência histórica. A versão atual é store-day-closed — v1.
store.business_day_closed dispara quando o pipeline automatizado de fechamento de dia do Fire termina de construir o snapshot imutável do dia operacional de uma loja. O payload é uma projeção direta desse snapshot mais o bloco store canônico v4 — os consumers (Kinesis, ERP, BI) montam seu próprio shape sem precisar fazer queries de volta ao Fire. Diferente dos eventos de pedido, este é um evento a nível loja: um por loja fechada, não um por pedido. O snapshot contém métricas agregadas, breakdowns por canal/forma de pagamento, detalhe de pedidos force-closed e detalhe de cancelamentos do dia.

Condição de disparo

O Fire emite store.business_day_closed uma vez por snapshot (storeId, businessDayDate), na primeira vez em que todas estas condições se cumprem:
  • A loja tinha um dia operacional aberto (isBusinessDayOpen === true) para a data a fechar
  • O pipeline de fechamento (CloseBusinessDayService.execute()) completou os 9 passos sem erro (force-close, cálculo de métricas, persistência do snapshot)
  • Não existe já um snapshot para esse par (storeId, businessDayDate) (guard de idempotência)
CoberturaUniversal — todos os países. Diferente de fiscal.*.br, sem gating por país.
Chave de idempotênciaevent.id (também trigger.data.snapshotId)
Dispara mais de uma vezNão, salvo retry. Um snapshot por (storeId, data).
OrigemPipeline cron (v1). O path de fechamento manual ainda não emite.
Latência vs fechamento realSíncrono — o evento é enfileirado imediatamente após saveSnapshot() retornar.

O que está em trigger.data

O V4StoreDayClosedSnapshot completo — 17 keys top-level cobrindo identidade, o bloco store canônico, agregados de sales/metrics/summary, breakdowns por canal e por forma de pagamento, detalhe de pedidos force-closed, detalhe de cancelamentos, e metadata extensível.

Exemplo — payload real (sanitizado)

{
  "event": {
    "id": "43214dd1-e260-4245-945d-5bbdfc4a54be",
    "type": "store.business_day_closed",
    "createdAt": "2026-05-18T21:59:01.306Z"
  },
  "data": {
    "snapshotId": "2c332116-79ec-4c63-a53d-8b87fff5a654",
    "businessDayDate": "2026-03-31",
    "timezone": "America/Sao_Paulo",
    "status": "CLOSED",
    "openedAt": "2026-03-31T09:00:02.55+00:00",
    "closedAt": "2026-05-18T21:58:09.882+00:00",

    "closedBy": {
      "uid": "00000000-0000-0000-0000-000000000000",
      "name": "Cierre Automático — Sandbox",
      "type": "system"
    },

    "store": {
      "uid": "a4019cad-bbac-4269-9f8d-f29654e92c45",
      "code": "BR-SP-001",
      "name": "Sample Store SP",
      "externalId": "200400500",
      "phone": "+551155512042",
      "address": "Av. Paulista 1578, Bela Vista, São Paulo - SP",
      "locationInfo": { "country": { "code": "BR" }, "city": { "code": "SAO" }, "timezone": "America/Sao_Paulo", "currencyCode": "BRL" },
      "vendor": { "uid": "100.2.1", "name": "Deli Burger BR", "description": null, "loyaltyPlan": false },
      "account": { "uid": "100", "name": "Sandbox", "description": null },
      "storeFiscalConfig": { "enabled": true, "company": { "govIdType": "CNPJ", "legalName": "Dev Company" }, "metadata": { "storeCode3S": "50000001" } }
    },

    "summary": { "total_orders": 48, "completed_orders": 40, "open_orders": 2, "cancelled_orders": 6, "close_percentage": 95.2 },
    "sales":   { "gross": 1741, "net": 1512.34, "taxes": 228.66, "discounts": 0, "currency": "BRL", "order_count": 40 },
    "metrics": { "average_order_value": 44, "operation_minutes": 887, "first_order_at": "2026-03-31T12:13:04.000Z", "last_order_at": "2026-03-31T23:59:48.000Z", "peak_hour": "20:00", "peak_hour_orders": 23, "payment_failures": 1 },

    "byChannel": [
      { "channel_id": "APP", "channel_name": "Sabor App", "order_count": 30, "total": 1240.50, "percentage": 71.2, "by_fulfillment": [{ "fulfillment_type": "DELIVERY", "order_count": 30, "total": 1240.50, "percentage": 100 }] },
      { "channel_id": "POS", "channel_name": "Balcão",    "order_count": 10, "total": 500.50,  "percentage": 28.8, "by_fulfillment": [{ "fulfillment_type": "DINE_IN",  "order_count": 10, "total": 500.50,  "percentage": 100 }] }
    ],
    "byPaymentMethod": [
      { "processor": "CARD", "transaction_count": 28, "total": 1200.00, "percentage": 68.9 },
      { "processor": "PIX",  "transaction_count": 12, "total": 541.00,  "percentage": 31.1 }
    ],

    "forceClosedOrders": [
      { "order_id": "1dda...", "order_uid": "XM-2026-03-31-871", "payment_status": "PENDING", "channel_id": "APP", "total": 42.0, "created_at": "2026-03-31T23:47:12.000Z", "closure_reason": "PAYMENT_PENDING", "payment_methods": [{ "processor": "PIX", "status": "PENDING", "total": 0 }] },
      { "order_id": "1633...", "order_uid": "XM-2026-03-31-872", "payment_status": "FAILED",  "channel_id": "POS", "total": 87.5, "created_at": "2026-03-31T23:51:33.000Z", "closure_reason": "PAYMENT_FAILED",  "payment_methods": [{ "processor": "CARD", "status": "FAILED", "total": 87.5, "metadata": { "reason": "DECLINED" } }] }
    ],
    "closureStats": { "total_force_closed": 2, "total_lost_revenue": 129.5, "by_reason": { "abandoned": 0, "payment_pending": 1, "payment_failed": 1, "partial_payment": 0, "integration_error": 0 } },

    "cancelledOrders": [
      { "order_id": "e98f...", "order_uid": "ac331b68-66fb-...", "order_code": "OC-001", "cancelled_at": "2026-03-31T19:42:18.000Z", "cancellation_reason": "Cliente desistiu — item indisponível", "cancellation_source": "backoffice", "total": 96.30, "currency": "BRL" }
    ],

    "metadata": { "cash_reconciliation_summary": { "by_currency": [], "total_match": 0, "total_overage": 0, "total_shortage": 0, "total_reconciliations": 0 } }
  },
  "_meta": { "executionId": "...", "flowId": "...", "flowName": "...", "attempt": 0, "triggerEntityId": "2c332116-..." }
}

Referência de data.*

Identidade & temporal

snapshotId
string
UUID do snapshot persistido no DB do Fire. Também exposto como _meta.triggerEntityId. Use como chave de idempotência do seu lado — re-entregas do mesmo fechamento compartilham o mesmo snapshotId.
businessDayDate
string
Data calendário que o fechamento representa, no timezone da loja (ex. "2026-03-31"). Não é a data UTC em que o evento disparou.
timezone
string
Timezone IANA da loja (ex. "America/Sao_Paulo"). Use para interpretar businessDayDate e os timestamps metrics.*_order_at.
status
string
Sempre "CLOSED" para este evento. O campo existe por paridade com o shape do row do snapshot; não há outro valor possível aqui.
openedAt
string | null
Timestamp ISO 8601 UTC de quando o dia foi aberto. Pode ser null para lojas que nunca abriram explicitamente (data legacy); para lojas fechadas pelo cron v1 sempre está presente.
closedAt
string
Timestamp ISO 8601 UTC de quando o pipeline persistiu o snapshot. Use isto para medir SLI/SLO em vez de event.createdAt (que é o tempo de dispatch da fila).

closedBy — identidade do operador

closedBy
object
Identidade do operador já resolvida. Nenhum lookup runtime necessário downstream.

store — bloco store canônico

Mesmo shape que em order.completed — ver order.completed para a referência de campos. Pontos-chave:
  • store.externalId é o identificador controlado pelo Fire desde settings.externalId, não o que algum sistema POS tenha injetado. Use como chave de reconciliação do lado partner.
  • store.storeFiscalConfig viaja completo (CNPJ, IE, storeCode3S, etc. para BR; campos análogos para outros países). Credenciais nunca são incluídas.
  • store.vendor.name e store.account.name são populados desde o metadata de account do Fire. description e loyaltyPlan podem vir null/false aqui mesmo quando populados em eventos de pedido — esses campos vêm do payload POS injetado em eventos de pedido e não são parte do metadata de account do Fire hoje.

summary, sales, metrics

summary
object
Breakdown de contagem de pedidos do dia.
sales
object
Agregados de receita. Inclui apenas pedidos COMPLETED + SUCCEEDED na moeda dominante.
metrics
object
Indicadores de performance.

Breakdowns

byChannel
array
Sales por canal (APP, KIOSK, POS, WEB, …). Cada entrada inclui um sub-breakdown by_fulfillment (DELIVERY, PICKUP, DINE_IN, etc.). Apenas pedidos COMPLETED + SUCCEEDED.
byPaymentMethod
array
Sales por processador de pagamento (CASH, CARD, PIX, …). Agregado desde payment_methods[].processor através dos pedidos pagos.

Anomalias

forceClosedOrders
array
Detalhe de pedidos que estavam ainda OPEN no momento do fechamento. Cada entrada inclui order_id, order_uid, payment_status, total, o snapshot original de payment_methods[], e um closure_reason classificando por que o pedido não completou naturalmente.Valores possíveis de closure_reason: ABANDONED, PAYMENT_PENDING, PAYMENT_FAILED, PARTIAL_PAYMENT, INTEGRATION_ERROR.
closureStats
object
Stats agregados de force-close — contagem por razão e total de receita não cobrada (soma de forceClosedOrders[].total).
cancelledOrders
array
Detalhe por pedido de cancelamentos (order_id, order_uid, order_code, cancelled_at, cancellation_reason, cancellation_source de "adapter" ou "backoffice", total, currency). Para snapshots criados antes de este campo existir (data legacy), este array é [].
metadata
object | null
Bucket extensível. Hoje carrega cash_reconciliation_summary (sempre, mesmo se vazio) e opcionalmente currency_anomaly (quando mais de uma moeda foi detectada no dia). Pode crescer com o tempo sem quebrar o contrato — trate chaves desconhecidas como forward-compatíveis.

Exemplo de handler

async function onStoreDayClosed(data) {
  const { snapshotId, businessDayDate, store, sales, summary, closureStats, forceClosedOrders, cancelledOrders, closedBy } = data;

  // 1. Idempotência — pular se já processamos este snapshot
  const existing = await db.dayCloses.findUnique({ where: { snapshotId } });
  if (existing) return;

  // 2. Persistir agregados headline para BI
  await db.dayCloses.create({
    data: {
      snapshotId,
      storeExternalId: store.externalId,
      storeCode: store.code,
      businessDayDate,
      currency: sales.currency,
      gross: sales.gross,
      net: sales.net,
      orderCount: summary.completed_orders,
      forceClosedCount: closureStats.total_force_closed,
      cancelledCount: summary.cancelled_orders,
      closedByName: closedBy.name,
      closedByType: closedBy.type,
    },
  });

  // 3. Alertar sobre anomalias
  if (closureStats.total_force_closed > 5) {
    await alerts.send({
      severity: "warning",
      title: `${closureStats.total_force_closed} pedidos force-closed em ${store.code}`,
      detail: forceClosedOrders.map(o => `${o.order_uid}: ${o.closure_reason}`).join("\n"),
    });
  }

  // 4. Encaminhar cancelamentos ao audit trail (sem queries extras — detalhe vem embutido)
  for (const c of cancelledOrders) {
    await audit.cancellations.upsert({
      where: { orderId: c.order_id },
      create: { ...c, storeExternalId: store.externalId, businessDayDate },
      update: {},
    });
  }
}

Pontos de atenção

  • closedBy.uid pode ser o nil UUID "00000000-0000-0000-0000-000000000000" — para fechamentos system onde o account não configurou um system user custom. Use closedBy.type === "system" para detectar fechamentos system em vez de parsear uid.
  • sales.currency pode ser null quando a loja fechou sem pedidos pagos. Não assuma um default — bifurque sobre o null.
  • As keys de closureStats.by_reason estão sempre presentes, mesmo quando zero. Não filtre por presença; some/compare sobre os valores.
  • metrics.first_order_at / last_order_at / peak_hour* estão ausentes (não null, ausentes) quando a loja não teve pedidos. Use optional chaining.
  • cancelledOrders é [] para snapshots criados antes do detalhe por-pedido de cancelamentos ser adicionado. Se precisar disso em data histórica, consulte diretamente a API de snapshots do Fire — snapshots antigos não são backfilleados retroativamente.
  • store.vendor.description e store.vendor.loyaltyPlan podem vir null/false aqui mesmo quando populados em eventos a nível de pedido. Esses campos vêm do payload POS injetado em eventos de pedido e não são armazenados no metadata de account do Fire hoje.

Eventos relacionados

order.completed

Dispara por cada pedido individual ao completar. store.business_day_closed agrega muitos eventos order.completed.

order.cancelled

Evento por cancelamento individual. Cada cancelamento também aparece em cancelledOrders[] do fechamento do dia.