> ## 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

> A store has closed its business day — fires once per (store, businessDayDate) with a rich snapshot of the day's activity.

<Warning>
  **Deprecated (v0).** Previous contract, kept only as a historical reference. The current version is [store.day\_closed — v1](/en/events/store-day-closed).
</Warning>

`store.business_day_closed` fires when Fire's automated business-day-close pipeline finishes building the immutable snapshot for a store's operational day. The payload is a direct projection of that snapshot plus the canonical v4 `store` block — so consumers (Kinesis, ERP, BI) can construct their own shape **with zero queries back to Fire**.

Unlike order-level events, this is a **store-level** event: one per store closed, not one per order. The snapshot contains aggregate metrics, per-channel/payment breakdowns, force-closed order detail, and cancellation detail for the day.

## Trigger condition

Fire emits `store.business_day_closed` once per `(storeId, businessDayDate)` snapshot, the first time **all** of these are true:

* The store had an open business day (`isBusinessDayOpen === true`) for the date being closed
* The close pipeline (`CloseBusinessDayService.execute()`) completed all 9 steps without error (force-close, metric calculation, snapshot persistence)
* No snapshot already exists for that `(storeId, businessDayDate)` pair (idempotency guard)

|                                  |                                                                                 |
| -------------------------------- | ------------------------------------------------------------------------------- |
| Coverage                         | **Universal** — all countries. Unlike `fiscal.*.br`, no country gating.         |
| Idempotency key                  | `event.id` (also `trigger.data.snapshotId`)                                     |
| Fires more than once             | No, unless retried. One snapshot per (storeId, date).                           |
| Source                           | Cron pipeline (v1). Manual close path is not yet emitting.                      |
| Latency relative to actual close | Synchronous — the event is enqueued immediately after `saveSnapshot()` returns. |

## What's in `trigger.data`

The full `V4StoreDayClosedSnapshot` — 17 top-level keys covering identity, the canonical store block, aggregate sales/metrics/summary, per-channel and per-payment-method breakdowns, force-closed order detail, cancelled order detail, and extensible metadata.

## Example — real payload (sanitized)

```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": "Customer dropped — item unavailable", "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-..." }
}
```

## `data.*` reference

### Identity & temporal

<ResponseField name="snapshotId" type="string">
  UUID of the persisted snapshot row in Fire's database. Also surfaced as `_meta.triggerEntityId`. Use it as the idempotency key on your side — re-deliveries of the same close share the same `snapshotId`.
</ResponseField>

<ResponseField name="businessDayDate" type="string">
  Calendar date the close represents, in the store's timezone (e.g. `"2026-03-31"`). **Not** the UTC date the event fired.
</ResponseField>

<ResponseField name="timezone" type="string">
  IANA timezone of the store (e.g. `"America/Sao_Paulo"`). Use it to interpret `businessDayDate` and the `metrics.*_order_at` timestamps.
</ResponseField>

<ResponseField name="status" type="string">
  Always `"CLOSED"` for this event. The field exists to keep parity with the snapshot row shape; no other value is possible here.
</ResponseField>

<ResponseField name="openedAt" type="string | null">
  ISO 8601 UTC timestamp the business day opened. May be `null` for stores that never explicitly opened (legacy data); for v1 cron-closed stores it's always present.
</ResponseField>

<ResponseField name="closedAt" type="string">
  ISO 8601 UTC timestamp the close pipeline persisted the snapshot. Use this for SLI/SLO measurements rather than `event.createdAt` (which is the queue dispatch time).
</ResponseField>

### `closedBy` — operator identity

<ResponseField name="closedBy" type="object">
  Already-resolved operator identity. **No runtime lookups needed downstream.**

  <Expandable title="closedBy">
    <ResponseField name="uid" type="string">
      For `type: "system"` (v1): the `accounts.id` UUID (or a stable nil UUID `"00000000-0000-0000-0000-000000000000"` for accounts without explicit system-user config). For `type: "user"` (v2, not yet emitted): the user's UUID from Fire's auth table.
    </ResponseField>

    <ResponseField name="name" type="string">
      Human-readable label. For system: typically `"Cierre Automático — {accountName}"`. For user: the user's display name.
    </ResponseField>

    <ResponseField name="type" type="string">
      `"system"` for automated cron-driven closes (v1). `"user"` is reserved for manual-close flows (v2, not yet implemented).
    </ResponseField>
  </Expandable>
</ResponseField>

### `store` — canonical store block

Same shape as in `order.completed` — see [`order.completed`](/en/events/order-completed#store) for the field reference. Key callouts:

* `store.externalId` is the Fire-controlled identifier from `settings.externalId`, not whatever any POS system happened to inject. Use it as the partner-side reconciliation key.
* `store.storeFiscalConfig` travels complete (CNPJ, IE, `storeCode3S`, etc. for BR; analogous fields for other countries). Credentials are never included.
* `store.vendor.name` and `store.account.name` are populated from Fire's account metadata. `description` and `loyaltyPlan` may be `null`/`false` here even when populated in order events — those fields come from the injected POS payload in order events and are not part of Fire's account metadata today.

### `summary`, `sales`, `metrics`

<ResponseField name="summary" type="object">
  Order count breakdown for the day.

  <Expandable title="summary">
    <ResponseField name="total_orders" type="number">All orders matching the store + businessDayDate, regardless of status.</ResponseField>
    <ResponseField name="completed_orders" type="number">Orders with `status === "COMPLETED"`.</ResponseField>
    <ResponseField name="open_orders" type="number">Orders that were still `OPEN` at close time. These were force-closed and appear in `forceClosedOrders[]`.</ResponseField>
    <ResponseField name="cancelled_orders" type="number">Orders with `status === "CANCELLED"`. Detail in `cancelledOrders[]`.</ResponseField>
    <ResponseField name="close_percentage" type="number">`completed / (total - cancelled) * 100`, 1 decimal. The day's "health" metric.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="sales" type="object">
  Revenue aggregates. **Includes only `COMPLETED` + `SUCCEEDED` orders** in the dominant currency.

  <Expandable title="sales">
    <ResponseField name="gross" type="number">Total including taxes.</ResponseField>
    <ResponseField name="net" type="number">Subtotal without taxes.</ResponseField>
    <ResponseField name="taxes" type="number">Tax amount.</ResponseField>
    <ResponseField name="discounts" type="number">Discount amount.</ResponseField>
    <ResponseField name="currency" type="string | null">ISO 4217 currency code (`"USD"`, `"BRL"`, `"COP"`, …). `null` when no paid orders.</ResponseField>
    <ResponseField name="order_count" type="number">Paid orders contributing to the dominant-currency sales.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="metrics" type="object">
  Performance indicators.

  <Expandable title="metrics">
    <ResponseField name="average_order_value" type="number">`sales.gross / sales.order_count`, rounded.</ResponseField>
    <ResponseField name="orders_per_minute" type="number">`summary.total_orders / metrics.operation_minutes`, 2 decimals.</ResponseField>
    <ResponseField name="operation_minutes" type="number">Minutes between first and last order timestamps.</ResponseField>
    <ResponseField name="first_order_at" type="string?">ISO timestamp. Omitted when there were no orders.</ResponseField>
    <ResponseField name="last_order_at" type="string?">ISO timestamp. Omitted when there were no orders.</ResponseField>
    <ResponseField name="peak_hour" type="string?">Hour with most orders, formatted `"HH:00"`. Omitted when no orders.</ResponseField>
    <ResponseField name="peak_hour_orders" type="number?">Order count in the peak hour.</ResponseField>
    <ResponseField name="payment_failures" type="number">Orders whose payment ended in `FAILED`.</ResponseField>
  </Expandable>
</ResponseField>

### Breakdowns

<ResponseField name="byChannel" type="array">
  Sales by channel (APP, KIOSK, POS, WEB, …). Each entry includes a `by_fulfillment` sub-breakdown (DELIVERY, PICKUP, DINE\_IN, etc.). Only `COMPLETED` + `SUCCEEDED` orders.
</ResponseField>

<ResponseField name="byPaymentMethod" type="array">
  Sales by payment processor (`CASH`, `CARD`, `PIX`, …). Aggregated from `payment_methods[].processor` across paid orders.
</ResponseField>

### Anomalies

<ResponseField name="forceClosedOrders" type="array">
  Detail of orders that were still `OPEN` at close time. Each entry includes `order_id`, `order_uid`, `payment_status`, `total`, the original `payment_methods[]` snapshot, and a `closure_reason` classifying why the order didn't complete naturally.

  Possible `closure_reason` values: `ABANDONED`, `PAYMENT_PENDING`, `PAYMENT_FAILED`, `PARTIAL_PAYMENT`, `INTEGRATION_ERROR`.
</ResponseField>

<ResponseField name="closureStats" type="object">
  Aggregate force-close stats — count per reason and total uncollected revenue (sum of `forceClosedOrders[].total`).
</ResponseField>

<ResponseField name="cancelledOrders" type="array">
  Per-order cancellation detail (`order_id`, `order_uid`, `order_code`, `cancelled_at`, `cancellation_reason`, `cancellation_source` of `"adapter"` or `"backoffice"`, `total`, `currency`). For snapshots created before this field existed (legacy data), this array is `[]`.
</ResponseField>

<ResponseField name="metadata" type="object | null">
  Extensible bucket. Today carries `cash_reconciliation_summary` (always, even if empty) and optionally `currency_anomaly` (when more than one currency was detected in the day). May grow over time without breaking the contract — treat unknown keys as forward-compatible.
</ResponseField>

## Handler example

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

  // 1. Idempotency — skip if we already processed this snapshot
  const existing = await db.dayCloses.findUnique({ where: { snapshotId } });
  if (existing) return;

  // 2. Persist the headline aggregates for 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. Alert on anomalies
  if (closureStats.total_force_closed > 5) {
    await alerts.send({
      severity: "warning",
      title: `${closureStats.total_force_closed} orders force-closed at ${store.code}`,
      detail: forceClosedOrders.map(o => `${o.order_uid}: ${o.closure_reason}`).join("\n"),
    });
  }

  // 4. Forward cancellations to the audit trail (no extra query — detail is embedded)
  for (const c of cancelledOrders) {
    await audit.cancellations.upsert({
      where: { orderId: c.order_id },
      create: { ...c, storeExternalId: store.externalId, businessDayDate },
      update: {},
    });
  }
}
```

## Common pitfalls

* **`closedBy.uid` may be the nil UUID `"00000000-0000-0000-0000-000000000000"`** — for system closes where the account hasn't configured a custom system user. Use `closedBy.type === "system"` to detect system closes rather than parsing `uid`.
* **`sales.currency` can be `null`** when the store closed without any paid orders. Don't assume a default — branch on the null.
* **`closureStats.by_reason` keys are always present**, even when zero. Don't filter for presence; sum/compare on the values.
* **`metrics.first_order_at` / `last_order_at` / `peak_hour*` are absent** (not `null`, **absent**) when the store had no orders. Use optional chaining.
* **`cancelledOrders` is `[]` for snapshots from before the per-order cancellation detail was added.** If you need it on historical data, query Fire's snapshot API directly — older snapshots are not retroactively backfilled.
* **`store.vendor.description` and `store.vendor.loyaltyPlan` may be `null`/`false` here** even when populated in order-level events. These fields originate from the POS-injected payload in order events and aren't stored in Fire's account metadata today.

## Related events

<CardGroup cols={2}>
  <Card title="order.completed" icon="receipt" href="/en/events/order-completed">
    Fires per individual order as it completes. `store.business_day_closed` aggregates many `order.completed` events.
  </Card>

  <Card title="order.cancelled" icon="circle-xmark" href="/en/events/order-cancelled">
    Per-cancellation event. Each cancellation also appears in `cancelledOrders[]` of the day's close.
  </Card>
</CardGroup>
