> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fire.rest/llms.txt
> Use this file to discover all available pages before exploring further.

# store.business_day_closed

> Una tienda cerró su día operativo — se dispara una vez por (store, businessDayDate) con un snapshot completo de la actividad del día.

<Warning>
  **Deprecado (v0).** Contrato anterior, se mantiene solo como referencia histórica. La versión actual es [store.day\_closed — v1](/es/events/store-day-closed).
</Warning>

`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)

|                           |                                                                                        |
| ------------------------- | -------------------------------------------------------------------------------------- |
| Cobertura                 | **Universal** — todos los países. A diferencia de `fiscal.*.br`, sin gating por país.  |
| Clave de idempotencia     | `event.id` (también `trigger.data.snapshotId`)                                         |
| Se dispara más de una vez | No, salvo retry. Un snapshot por (storeId, fecha).                                     |
| Origen                    | Pipeline cron (v1). El path de cierre manual aún no emite.                             |
| Latencia vs cierre real   | Sí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)

```json theme={null}
{
  "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

<ResponseField name="snapshotId" type="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`.
</ResponseField>

<ResponseField name="businessDayDate" type="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.
</ResponseField>

<ResponseField name="timezone" type="string">
  Timezone IANA de la tienda (ej. `"America/Sao_Paulo"`). Úsala para interpretar `businessDayDate` y los timestamps `metrics.*_order_at`.
</ResponseField>

<ResponseField name="status" type="string">
  Siempre `"CLOSED"` para este evento. El campo existe por paridad con el shape del row del snapshot; no hay otro valor posible aquí.
</ResponseField>

<ResponseField name="openedAt" type="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.
</ResponseField>

<ResponseField name="closedAt" type="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).
</ResponseField>

### `closedBy` — identidad del operador

<ResponseField name="closedBy" type="object">
  Identidad del operador ya resuelta. **No hace falta hacer lookups en runtime downstream.**

  <Expandable title="closedBy">
    <ResponseField name="uid" type="string">
      Para `type: "system"` (v1): el UUID de `accounts.id` (o un nil UUID estable `"00000000-0000-0000-0000-000000000000"` para accounts sin config explícita de system user). Para `type: "user"` (v2, aún no emitido): el UUID del usuario de la tabla de auth de Fire.
    </ResponseField>

    <ResponseField name="name" type="string">
      Label legible. Para system: típicamente `"Cierre Automático — {accountName}"`. Para user: el display name del usuario.
    </ResponseField>

    <ResponseField name="type" type="string">
      `"system"` para cierres automatizados por cron (v1). `"user"` queda reservado para flujos de cierre manual (v2, no implementado todavía).
    </ResponseField>
  </Expandable>
</ResponseField>

### `store` — bloque store canónico

Mismo shape que en `order.completed` — ver [`order.completed`](/es/events/order-completed#store) 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`

<ResponseField name="summary" type="object">
  Breakdown de conteo de órdenes del día.

  <Expandable title="summary">
    <ResponseField name="total_orders" type="number">Todas las órdenes que matchean store + businessDayDate, sin importar status.</ResponseField>
    <ResponseField name="completed_orders" type="number">Órdenes con `status === "COMPLETED"`.</ResponseField>
    <ResponseField name="open_orders" type="number">Órdenes que seguían `OPEN` al momento del cierre. Fueron force-closed y aparecen en `forceClosedOrders[]`.</ResponseField>
    <ResponseField name="cancelled_orders" type="number">Órdenes con `status === "CANCELLED"`. Detalle en `cancelledOrders[]`.</ResponseField>
    <ResponseField name="close_percentage" type="number">`completed / (total - cancelled) * 100`, 1 decimal. La métrica de "salud" del día.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="sales" type="object">
  Agregados de ingresos. **Incluye solo órdenes `COMPLETED` + `SUCCEEDED`** en la moneda dominante.

  <Expandable title="sales">
    <ResponseField name="gross" type="number">Total incluyendo impuestos.</ResponseField>
    <ResponseField name="net" type="number">Subtotal sin impuestos.</ResponseField>
    <ResponseField name="taxes" type="number">Monto de impuestos.</ResponseField>
    <ResponseField name="discounts" type="number">Monto de descuentos.</ResponseField>
    <ResponseField name="currency" type="string | null">Código de moneda ISO 4217 (`"USD"`, `"BRL"`, `"COP"`, …). `null` cuando no hubo órdenes pagas.</ResponseField>
    <ResponseField name="order_count" type="number">Órdenes pagas que contribuyen al sales de la moneda dominante.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="metrics" type="object">
  Indicadores de performance.

  <Expandable title="metrics">
    <ResponseField name="average_order_value" type="number">`sales.gross / sales.order_count`, redondeado.</ResponseField>
    <ResponseField name="orders_per_minute" type="number">`summary.total_orders / metrics.operation_minutes`, 2 decimales.</ResponseField>
    <ResponseField name="operation_minutes" type="number">Minutos entre los timestamps de primera y última orden.</ResponseField>
    <ResponseField name="first_order_at" type="string?">Timestamp ISO. Ausente cuando no hubo órdenes.</ResponseField>
    <ResponseField name="last_order_at" type="string?">Timestamp ISO. Ausente cuando no hubo órdenes.</ResponseField>
    <ResponseField name="peak_hour" type="string?">Hora con más órdenes, formato `"HH:00"`. Ausente cuando no hay órdenes.</ResponseField>
    <ResponseField name="peak_hour_orders" type="number?">Conteo de órdenes en la hora pico.</ResponseField>
    <ResponseField name="payment_failures" type="number">Órdenes cuyo pago terminó en `FAILED`.</ResponseField>
  </Expandable>
</ResponseField>

### Breakdowns

<ResponseField name="byChannel" type="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`.
</ResponseField>

<ResponseField name="byPaymentMethod" type="array">
  Sales por procesador de pago (`CASH`, `CARD`, `PIX`, …). Agregado desde `payment_methods[].processor` a través de las órdenes pagas.
</ResponseField>

### Anomalías

<ResponseField name="forceClosedOrders" type="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`.
</ResponseField>

<ResponseField name="closureStats" type="object">
  Stats agregados de force-close — conteo por razón y total de ingresos no cobrados (suma de `forceClosedOrders[].total`).
</ResponseField>

<ResponseField name="cancelledOrders" type="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 `[]`.
</ResponseField>

<ResponseField name="metadata" type="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.
</ResponseField>

## Ejemplo de handler

```js theme={null}
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

<CardGroup cols={2}>
  <Card title="order.completed" icon="receipt" href="/es/events/order-completed">
    Se dispara por cada orden individual al completarse. `store.business_day_closed` agrega muchos eventos `order.completed`.
  </Card>

  <Card title="order.cancelled" icon="circle-xmark" href="/es/events/order-cancelled">
    Evento por cancelación individual. Cada cancelación también aparece en `cancelledOrders[]` del cierre del día.
  </Card>
</CardGroup>
