Skip to main content
Deprecated (v0). Previous contract, kept only as a historical reference. The current version is store-day-closed — v1.
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)
CoverageUniversal — all countries. Unlike fiscal.*.br, no country gating.
Idempotency keyevent.id (also trigger.data.snapshotId)
Fires more than onceNo, unless retried. One snapshot per (storeId, date).
SourceCron pipeline (v1). Manual close path is not yet emitting.
Latency relative to actual closeSynchronous — 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)

{
  "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

snapshotId
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.
businessDayDate
string
Calendar date the close represents, in the store’s timezone (e.g. "2026-03-31"). Not the UTC date the event fired.
timezone
string
IANA timezone of the store (e.g. "America/Sao_Paulo"). Use it to interpret businessDayDate and the metrics.*_order_at timestamps.
status
string
Always "CLOSED" for this event. The field exists to keep parity with the snapshot row shape; no other value is possible here.
openedAt
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.
closedAt
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).

closedBy — operator identity

closedBy
object
Already-resolved operator identity. No runtime lookups needed downstream.

store — canonical store block

Same shape as in order.completed — see order.completed 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

summary
object
Order count breakdown for the day.
sales
object
Revenue aggregates. Includes only COMPLETED + SUCCEEDED orders in the dominant currency.
metrics
object
Performance indicators.

Breakdowns

byChannel
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.
byPaymentMethod
array
Sales by payment processor (CASH, CARD, PIX, …). Aggregated from payment_methods[].processor across paid orders.

Anomalies

forceClosedOrders
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.
closureStats
object
Aggregate force-close stats — count per reason and total uncollected revenue (sum of forceClosedOrders[].total).
cancelledOrders
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 [].
metadata
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.

Handler example

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.

order.completed

Fires per individual order as it completes. store.business_day_closed aggregates many order.completed events.

order.cancelled

Per-cancellation event. Each cancellation also appears in cancelledOrders[] of the day’s close.