Skip to main content
Deprecado (v0). Contrato anterior, se mantiene solo como referencia histórica. La versión actual es store-day-closed — v1.
store.business_day_closed se dispara cuando el pipeline automatizado de cierre de día de Fire termina de construir el snapshot inmutable del día operativo de una tienda. El payload es una proyección directa de ese snapshot más el bloque store canónico v4 — los consumers (Kinesis, ERP, BI) arman su propio shape sin necesidad de hacer queries de regreso a Fire. A diferencia de los eventos de orden, este es un evento a nivel store: uno por tienda cerrada, no uno por orden. El snapshot contiene métricas agregadas, breakdowns por canal/método de pago, detalle de órdenes force-closed y detalle de cancelaciones del día.

Condición de disparo

Fire emite store.business_day_closed una vez por snapshot (storeId, businessDayDate), la primera vez que todas estas condiciones se cumplen:
  • La tienda tenía un día operativo abierto (isBusinessDayOpen === true) para la fecha a cerrar
  • El pipeline de cierre (CloseBusinessDayService.execute()) completó los 9 pasos sin error (force-close, cálculo de métricas, persistencia del snapshot)
  • No existe ya un snapshot para ese par (storeId, businessDayDate) (guard de idempotencia)
CoberturaUniversal — todos los países. A diferencia de fiscal.*.br, sin gating por país.
Clave de idempotenciaevent.id (también trigger.data.snapshotId)
Se dispara más de una vezNo, salvo retry. Un snapshot por (storeId, fecha).
OrigenPipeline cron (v1). El path de cierre manual aún no emite.
Latencia vs cierre realSíncrono — el evento se encola inmediatamente después de que saveSnapshot() retorna.

Qué hay en trigger.data

El V4StoreDayClosedSnapshot completo — 17 keys top-level cubriendo identidad, el bloque store canónico, agregados de sales/metrics/summary, breakdowns por canal y por método de pago, detalle de órdenes force-closed, detalle de cancelaciones, y metadata extensible.

Ejemplo — 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 desistió — ítem no disponible", "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-..." }
}

Referencia de data.*

Identidad y temporal

snapshotId
string
UUID del snapshot persistido en la DB de Fire. También expuesto como _meta.triggerEntityId. Úsalo como clave de idempotencia de tu lado — re-entregas del mismo cierre comparten el mismo snapshotId.
businessDayDate
string
Fecha calendario que representa el cierre, en la timezone de la tienda (ej. "2026-03-31"). No es la fecha UTC en la que se disparó el evento.
timezone
string
Timezone IANA de la tienda (ej. "America/Sao_Paulo"). Úsala para interpretar businessDayDate y los timestamps metrics.*_order_at.
status
string
Siempre "CLOSED" para este evento. El campo existe por paridad con el shape del row del snapshot; no hay otro valor posible aquí.
openedAt
string | null
Timestamp ISO 8601 UTC de cuando se abrió el día. Puede ser null para tiendas que nunca abrieron explícitamente (data legacy); para tiendas cerradas por el cron v1 siempre está presente.
closedAt
string
Timestamp ISO 8601 UTC de cuando el pipeline persistió el snapshot. Úsalo para medir SLI/SLO en vez de event.createdAt (que es el tiempo de despacho de la cola).

closedBy — identidad del operador

closedBy
object
Identidad del operador ya resuelta. No hace falta hacer lookups en runtime downstream.

store — bloque store canónico

Mismo shape que en order.completed — ver order.completed para la referencia de campos. Aclaraciones clave:
  • store.externalId es el identificador controlado por Fire desde settings.externalId, no lo que el sistema POS haya inyectado. Úsalo como clave de reconciliación del lado partner.
  • store.storeFiscalConfig viaja completo (CNPJ, IE, storeCode3S, etc. para BR; campos análogos para otros países). Las credenciales nunca se incluyen.
  • store.vendor.name y store.account.name se pueblan desde el metadata de account de Fire. description y loyaltyPlan pueden venir null/false aquí incluso cuando están poblados en eventos de orden — esos campos vienen del payload POS inyectado en eventos de orden y no son parte del metadata de account de Fire hoy.

summary, sales, metrics

summary
object
Breakdown de conteo de órdenes del día.
sales
object
Agregados de ingresos. Incluye solo órdenes COMPLETED + SUCCEEDED en la moneda dominante.
metrics
object
Indicadores de performance.

Breakdowns

byChannel
array
Sales por canal (APP, KIOSK, POS, WEB, …). Cada entrada incluye un sub-breakdown by_fulfillment (DELIVERY, PICKUP, DINE_IN, etc.). Solo órdenes COMPLETED + SUCCEEDED.
byPaymentMethod
array
Sales por procesador de pago (CASH, CARD, PIX, …). Agregado desde payment_methods[].processor a través de las órdenes pagas.

Anomalías

forceClosedOrders
array
Detalle de órdenes que seguían OPEN al momento del cierre. Cada entrada incluye order_id, order_uid, payment_status, total, el snapshot original de payment_methods[], y un closure_reason clasificando por qué la orden no se completó naturalmente.Valores posibles de closure_reason: ABANDONED, PAYMENT_PENDING, PAYMENT_FAILED, PARTIAL_PAYMENT, INTEGRATION_ERROR.
closureStats
object
Stats agregados de force-close — conteo por razón y total de ingresos no cobrados (suma de forceClosedOrders[].total).
cancelledOrders
array
Detalle por orden de cancelaciones (order_id, order_uid, order_code, cancelled_at, cancellation_reason, cancellation_source de "adapter" o "backoffice", total, currency). Para snapshots creados antes de que este campo existiera (data legacy), este array es [].
metadata
object | null
Bucket extensible. Hoy lleva cash_reconciliation_summary (siempre, incluso si vacío) y opcionalmente currency_anomaly (cuando se detectó más de una moneda en el día). Puede crecer con el tiempo sin romper el contrato — trata claves desconocidas como forward-compatibles.

Ejemplo de handler

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

  // 1. Idempotencia — saltar si ya procesamos 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 anomalías
  if (closureStats.total_force_closed > 5) {
    await alerts.send({
      severity: "warning",
      title: `${closureStats.total_force_closed} órdenes force-closed en ${store.code}`,
      detail: forceClosedOrders.map(o => `${o.order_uid}: ${o.closure_reason}`).join("\n"),
    });
  }

  // 4. Reenviar cancelaciones al audit trail (sin queries extra — el detalle viene embebido)
  for (const c of cancelledOrders) {
    await audit.cancellations.upsert({
      where: { orderId: c.order_id },
      create: { ...c, storeExternalId: store.externalId, businessDayDate },
      update: {},
    });
  }
}

Gotchas comunes

  • closedBy.uid puede ser el nil UUID "00000000-0000-0000-0000-000000000000" — para cierres system donde el account no configuró un system user custom. Usa closedBy.type === "system" para detectar cierres system en vez de parsear uid.
  • sales.currency puede ser null cuando la tienda cerró sin órdenes pagas. No asumas un default — bifurca sobre el null.
  • Las keys de closureStats.by_reason siempre están presentes, incluso cuando son cero. No filtres por presencia; suma/compara sobre los valores.
  • metrics.first_order_at / last_order_at / peak_hour* están ausentes (no null, ausentes) cuando la tienda no tuvo órdenes. Usa optional chaining.
  • cancelledOrders es [] para snapshots creados antes de que se agregara el detalle por-orden de cancelaciones. Si lo necesitas en data histórica, consulta directamente la API de snapshots de Fire — los snapshots viejos no se backfillean retroactivamente.
  • store.vendor.description y store.vendor.loyaltyPlan pueden venir null/false acá incluso cuando están poblados en eventos a nivel de orden. Esos campos vienen del payload POS inyectado en eventos de orden y no se almacenan en el metadata de account de Fire hoy.

Eventos relacionados

order.completed

Se dispara por cada orden individual al completarse. store.business_day_closed agrega muchos eventos order.completed.

order.cancelled

Evento por cancelación individual. Cada cancelación también aparece en cancelledOrders[] del cierre del día.