Skip to main content

Documentation Index

Fetch the complete documentation index at: https://apidocs.restauranteropro.com/llms.txt

Use this file to discover all available pages before exploring further.

Introducción

Qué es Restaurantero Pro

Restaurantero Pro es una plataforma de inteligencia financiera y operacional para restaurantes y bares. No es un POS — es la capa analítica que se instala encima de cualquier POS. Tu POS envía los datos transaccionales del día (ventas, pagos, cortes, productos vendidos). Restaurantero Pro los procesa, los convierte en inteligencia y los devuelve como:
  • Estado de Resultados (P&L) en tiempo real
  • Food Cost, Beverage Cost, Labor Cost vs benchmarks del sector
  • Estado de Cuenta de Efectivo (libro de caja digital)
  • Reconciliación de caja por turno y por día
  • Matriz de Menú (cuáles platillos son Estrella, Puzzle, Caballo de batalla o Perro)
  • Alertas automáticas cuando un KPI cruza el umbral de riesgo
  • Tiendita Inteligente — lista de compra basada en ventas reales + stock disponible
  • Agente IA “Controllero Financiero” que responde preguntas financieras en español mexicano

A quién va dirigida esta API

Esta documentación es para desarrolladores de sistemas POS que quieren conectar su plataforma a Restaurantero Pro. Si eres Carlos (dev de Restaurant Controller), la Sección 6 está escrita específicamente para ti, con el mapeo exacto de los modelos de RC a los endpoints de Restaurantero Pro.

Filosofía de integración

POS                     Restaurantero Pro
────────────────────    ────────────────────────────────────────
Datos brutos      →     Inteligencia procesada
Corte Z           →     P&L del día, food cost, alertas
Líneas de orden   →     Matriz de menú, product mix
Pagos             →     Reconciliación, flujo de efectivo
Cancelaciones     →     Void alerts, análisis de pérdidas
La integración es unidireccional en la ingesta (tu POS envía, nosotros recibimos) y bidireccional en la inteligencia (puedes consumir los datos procesados de vuelta para mostrarlos en tu POS o app).

Environments

EnvironmentBase URLPropósito
Producciónhttps://api.restauranteropro.com/api/v1Único ambiente disponible actualmente
Por ahora solo existe el ambiente de producción. Para pruebas de integración se utiliza una API Key con prefijo rpro_test_ que apunta al mismo servidor. Los datos de prueba se generan en la misma base de datos — coordina con el equipo de Restaurantero Pro para limpiarlos después de tus pruebas.

Versionado

La API usa prefijo de versión en la URL (/v1/). Cuando haya cambios breaking, se publicará una versión nueva (/v2/) con un período de convivencia mínimo de 90 días antes de deprecar la anterior. Los cambios no-breaking (campos nuevos opcionales, endpoints nuevos) se agregan sin cambio de versión.

Soporte para partners

  • Email: dev@restauranteropro.com
  • Slack: canal #partners-api (acceso por invitación al registrarte como partner)
  • Status y uptime: https://status.restauranteropro.com

1. Autenticación

API Keys — mecanismo principal

Cada sucursal (branch) tiene su propia API Key. Esto permite que una misma cadena con 5 restaurantes tenga 5 keys independientes, cada una con sus propios datos aislados. Formato:
rpro_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   ← Producción (datos reales)
rpro_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   ← Pruebas (mismo servidor, misma BD)
Cómo se incluye en cada request:
X-API-Key: rpro_live_abc123xyz789...
Content-Type: application/json
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Cómo obtener una API Key:
  1. El operador del restaurante crea su cuenta en https://api.restauranteropro.com
  2. Va a Ajustes → Integraciones → API Keys
  3. Genera una key para la sucursal que quiere conectar
  4. Copia la key y la pega en la configuración de su POS
La API Key solo se muestra una vez al generarse. Si se pierde, se genera una nueva y la anterior se revoca automáticamente.

OAuth2 Bearer Token — flujo avanzado

Para integraciones que requieren autorización delegada (el usuario del POS autoriza explícitamente a Restaurantero Pro a leer sus datos), se soporta OAuth2 con Laravel Passport.
Authorization: Bearer {access_token}
El flujo completo de OAuth2 está disponible en el Dev Portal. Para la mayoría de integraciones de POS, la API Key es suficiente y más simple.

Rate limits por plan

PlanRequests/minutoRequests/mes
Starter10050,000
Growth500500,000
Pro2,000Ilimitado
EnterpriseSin límiteSin límite
Cuando se supera el rate limit, la API responde 429 Too Many Requests con el header Retry-After: {segundos}.

Errores de autenticación

CódigoSignificadoQué hacer
401 UnauthorizedAPI Key faltante o malformadaVerifica el header X-API-Key
403 ForbiddenAPI Key válida pero sin permiso para este endpointVerifica el plan contratado
429 Too Many RequestsRate limit excedidoEspera Retry-After segundos

2. Convenciones generales

Formato de requests y responses

Todos los requests y responses usan JSON. Sigue una estructura inspirada en JSON:API:
{
  "data": {
    "type": "sales",
    "attributes": {
      "branch_identifier": "uuid-aqui",
      "date": "2026-05-15",
      "total_sales_cents": 534550
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "pos_version": "3.2.1",
      "sent_at": "2026-05-15T23:45:00Z"
    }
  }
}

Dinero — siempre en centavos

Todos los campos monetarios son enteros que representan centavos. Nunca se envían decimales. El error más común al integrar es enviar un decimal como 534.50 en lugar de 53450.
$150.00 MXN  →  15000  (centavos)
$534.50 MXN  →  53450  (centavos)
$1,200.00 MXN → 120000  (centavos)
// CORRECTO
"total_sales_cents": 53450

// INCORRECTO — nunca envíes decimales
"total_sales": 534.50

Fechas y horas

  • Todas las fechas en formato ISO 8601: YYYY-MM-DD
  • Todos los timestamps en ISO 8601 con timezone: 2026-05-15T23:45:00-07:00
  • La timezone se toma de la configuración de la sucursal en Restaurantero Pro. Si la sucursal está en Hermosillo (sin cambio de horario), usar -07:00 todo el año.
  • Los períodos de consulta usan date_from y date_to como parámetros de query.

Idempotencia — obligatoria en todos los POST de ingesta

Todos los endpoints POST /api/v1/ingest/* requieren el header X-Idempotency-Key con un UUID v4 único por operación. Por qué es obligatorio: Si la red falla y tu POS reintenta el mismo request, el sistema procesará los datos exactamente una vez, aunque el request llegue 2 o 3 veces. Sin este header, podrías duplicar datos. Cómo funciona:
1. Tu POS genera un UUID v4 para el Corte Z del día
2. Lo guarda localmente asociado a ese corte
3. Lo incluye en el header X-Idempotency-Key
4. Si el request falla por red, reintenta con el MISMO UUID
5. Restaurantero Pro responde 200 con el resultado previo, sin procesar dos veces
Regla de negocio para RC: Usa el identifier (UUID) del CashRegisterOpening como Idempotency Key. Es único por corte y ya lo tienes disponible.
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Parámetros de query para endpoints de inteligencia

Todos los endpoints GET /api/v1/intelligence/* aceptan estos parámetros:
ParámetroTipoRequeridoDescripción
branch_identifierUUIDUUID de la sucursal
date_fromYYYY-MM-DDInicio del período
date_toYYYY-MM-DDFin del período
periodstringNoShortcut: today, week, month, yesterday

Estructura de respuesta exitosa

{
  "data": {
    "type": "pnl_summary",
    "attributes": { },
    "meta": {
      "period": { "from": "2026-05-01", "to": "2026-05-15" },
      "branch_identifier": "uuid",
      "snapshot_date": "2026-05-15T06:00:00Z",
      "benchmarks": {
        "food_cost_status": "green",
        "labor_cost_status": "yellow",
        "ebitda_status": "green"
      }
    }
  }
}

Estructura de error

{
  "error": {
    "code": "RPRO_ERR_422",
    "message": "El campo total_sales_cents es requerido.",
    "field": "data.attributes.total_sales_cents",
    "request_id": "req_abc123"
  }
}

Headers de respuesta

HeaderDescripción
X-Request-IdID único del request para soporte
X-RateLimit-LimitLímite de requests por minuto de tu plan
X-RateLimit-RemainingRequests restantes en la ventana actual
X-RateLimit-ResetTimestamp Unix cuando se resetea el contador
X-Idempotent-Replayedtrue si la respuesta es un replay de idempotencia

Cómo calcular los montos de venta

Todos los campos de ventas en los endpoints de ingesta deben reflejar los totales netos calculados por tu sistema POS — suma de órdenes pagadas menos descuentos, cancelaciones y voids. Nunca deben ser capturados manualmente por el cajero ni estimados.
La regla aplica a todos los endpoints de ingesta:
EndpointCampos afectados
POST /ingest/cash-registercash_sales, card_sales, transfer_sales en summaries[]
POST /ingest/salestotal_sales_cents, total_sales_no_iva_cents, food_sales_cents, beverage_sales_cents
POST /ingest/paymentsamount_cents por cada método de pago
POST /ingest/orderssubtotal_cents, net_total_cents por cada línea
Fórmula correcta para cada monto:
total_sales_cents     = Σ(órdenes pagadas) − descuentos − cancelaciones − voids
cash_sales_cents      = Σ(pagos en efectivo de órdenes pagadas)
card_sales_cents      = Σ(pagos con tarjeta de órdenes pagadas)
net_total_cents       = subtotal_cents − discount_cents  (por línea de orden)
El único dato que puede venir del cajero: cash_variance en el Corte Z — es la diferencia entre el efectivo que el sistema esperaba y lo que el cajero contó físicamente al cerrar. Este es el único campo que refleja una captura manual, y es precisamente lo que Restaurantero Pro usa para detectar faltantes y sobrantes de caja.
cash_variance = efectivo_contado_por_cajero − efectivo_esperado_por_sistema
Un valor negativo indica faltante. Un valor positivo indica sobrante.

3. Endpoints de Ingesta

Estos son los endpoints que tu POS implementa. Tu sistema llama a estos endpoints para enviarnos datos. Nosotros los procesamos y generamos la inteligencia.
El orden recomendado al cerrar un Corte Z es:
1. POST /api/v1/ingest/orders          # primero las líneas de lo que se vendió
2. POST /api/v1/ingest/discounts       # descuentos aplicados durante el turno
3. POST /api/v1/ingest/cancellations   # cancelaciones y voids del turno
4. POST /api/v1/ingest/payments        # cómo se pagó
5. POST /api/v1/ingest/sales           # resumen de ventas del corte
6. POST /api/v1/ingest/cash-register   # el corte en sí (X o Z) — este dispara el procesamiento
El Corte Z (cash-register con tipo zout) es el trigger principal. Al recibirlo, Restaurantero Pro regenera todos los cubos del día y activa las alertas correspondientes.

3.1 Corte de caja — POST /api/v1/ingest/cash-register

Registra un corte de caja. En Restaurant Controller existen dos tipos:
  • Corte X (xout): cierre de turno parcial — informativo, no cierra el día
  • Corte Z (zout): cierre del día — trigger principal que procesa todos los cubos
Este endpoint mapea directamente al modelo CashRegisterOpening de RC. Headers requeridos:
X-API-Key: rpro_live_xxx
X-Idempotency-Key: {CashRegisterOpening.identifier}
Content-Type: application/json
Body del request:
{
  "data": {
    "type": "cash-register",
    "attributes": {
      "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
      "closing_type": "zout",
      "start_at": "2026-05-15T07:00:00-07:00",
      "end_at": "2026-05-15T23:45:00-07:00",
      "status": "completed",
      "summaries": [
        { "type": "cash_sales",          "name": "Venta Efectivo",              "amount_cents": 125000 },
        { "type": "card_sales",          "name": "Venta Tarjeta",               "amount_cents": 409550 },
        { "type": "transfer_sales",      "name": "Transferencia",               "amount_cents": 0      },
        { "type": "tips_received",       "name": "Propinas Cobradas",           "amount_cents": 30000  },
        { "type": "tips_paid_to_waiter", "name": "Propinas Pagadas a Mesero",   "amount_cents": 22000  },
        { "type": "withholding",         "name": "Retención (Moche)",           "amount_cents": 5000   },
        { "type": "deposits",            "name": "Depósitos a Caja",            "amount_cents": 10000  },
        { "type": "cash_variance",       "name": "Sobrante / Faltante",         "amount_cents": 314    }
      ]
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "pos_version": "3.2.1",
      "pos_variant": "full_dining",
      "sent_at": "2026-05-15T23:46:00Z",
      "cashier_name": "Ana García",
      "cash_register_name": "CAJA 1"
    }
  }
}
Tipos de summary disponibles:
typeDescripción
cash_salesTotal vendido en efectivo
card_salesTotal vendido con tarjeta (crédito + débito)
card_credit_salesTarjeta crédito (opcional, desglose)
card_debit_salesTarjeta débito (opcional, desglose)
transfer_salesTransferencia bancaria
delivery_salesApps de delivery (Rappi, Uber Eats, DiDi Food)
voucher_salesVales de despensa
tips_receivedPropinas cobradas a clientes
tips_paid_to_waiterPropinas entregadas a meseros
withholdingRetención (moche al mesero)
depositsDepósitos al fondo de caja
cash_varianceDiferencia entre efectivo esperado y real (positivo = sobrante, negativo = faltante)
Response exitoso 202 Accepted:
{
  "data": {
    "type": "cash-register-ingest",
    "id": "ingest_uuid",
    "attributes": {
      "queued": true,
      "closing_type": "zout",
      "message": "Corte Z recibido. Los cubos del día se regenerarán en los próximos segundos."
    }
  }
}
La respuesta es 202 Accepted, no 200 OK. El procesamiento ocurre de forma asíncrona en cola. Usa webhooks (snapshot.completed) para saber cuándo los datos están listos para consultar.

3.2 Ventas — POST /api/v1/ingest/sales

Resumen de ventas del corte o del día. Complementa al cash-register con el desglose por categoría de venta. Body del request:
{
  "data": {
    "type": "sales",
    "attributes": {
      "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
      "date": "2026-05-15",
      "shift": "dinner",
      "closing_type": "zout",
      "total_sales_cents": 534550,
      "total_sales_no_iva_cents": 461681,
      "food_sales_cents": 366614,
      "beverage_sales_cents": 167936,
      "bar_sales_cents": 0,
      "covers": 87,
      "tables_served": 24,
      "avg_ticket_cents": 6144,
      "avg_pax_cents": 6144
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "pos_version": "3.2.1",
      "sent_at": "2026-05-15T23:46:00Z"
    }
  }
}
Valores válidos para shift:
ValorDescripción
breakfastDesayuno
lunchComida
dinnerCena
barBar / late night
all_dayCuando no se separa por turno
Response exitoso 202 Accepted:
{
  "data": {
    "type": "sales-ingest",
    "attributes": { "queued": true }
  }
}

3.3 Pagos por método — POST /api/v1/ingest/payments

Desglose de cómo se pagaron las ventas del día. Mapea los Payment de RC agrupados por PaymentType. Body del request:
{
  "data": {
    "type": "payments",
    "attributes": {
      "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
      "date": "2026-05-15",
      "closing_type": "zout",
      "payments": [
        {
          "payment_type": "cash",
          "payment_type_name": "Efectivo",
          "amount_cents": 125000,
          "tips_cents": 8000,
          "change_cents": 3500,
          "transaction_count": 18
        },
        {
          "payment_type": "card_credit",
          "payment_type_name": "Tarjeta Crédito",
          "amount_cents": 280000,
          "tips_cents": 15000,
          "change_cents": 0,
          "transaction_count": 31
        },
        {
          "payment_type": "card_debit",
          "payment_type_name": "Tarjeta Débito",
          "amount_cents": 129550,
          "tips_cents": 7000,
          "change_cents": 0,
          "transaction_count": 23
        }
      ]
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "sent_at": "2026-05-15T23:46:00Z"
    }
  }
}
Tipos de pago estándar:
payment_typeDescripción
cashEfectivo
card_creditTarjeta crédito
card_debitTarjeta débito
cardTarjeta (sin distinción crédito/débito)
transferTransferencia bancaria / SPEI
rappiRappi
uber_eatsUber Eats
didi_foodDiDi Food
voucherVales de despensa
otherOtro

3.4 Líneas de orden — POST /api/v1/ingest/orders

Envía los productos vendidos durante el período. Cada línea genera una fila independiente en la base de datos — esto permite queries directas para Top 10 productos, Star Employees (ranking de meseros) y Ventas por categoría en el dashboard. Mapea Order → OrderTicket → OrderTicketLine de RC. Body del request:
{
  "data": {
    "type": "orders",
    "attributes": {
      "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
      "date": "2026-05-15",
      "closing_type": "zout",
      "lines": [
        {
          "item_identifier": "item-uuid-1",
          "item_sku": "ARRACHERA-01",
          "item_name": "Arrachera a las Brasas",
          "category": "Cortes",
          "category_type": "food",
          "waiter_name": "Ana García",
          "quantity": 14,
          "unit_price_cents": 34500,
          "subtotal_cents": 483000,
          "discount_cents": 0,
          "net_total_cents": 483000
        },
        {
          "item_identifier": "item-uuid-2",
          "item_sku": "MARGARITA-01",
          "item_name": "Margarita Clásica",
          "category": "Cocteles",
          "category_type": "beverage",
          "waiter_name": "Luis Herrera",
          "quantity": 32,
          "unit_price_cents": 12000,
          "subtotal_cents": 384000,
          "discount_cents": 12000,
          "net_total_cents": 372000
        }
      ]
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "sent_at": "2026-05-15T23:45:00Z"
    }
  }
}
Campos por línea:
CampoTipoRequeridoDescripción
item_namestring✅ SíNombre del platillo o producto
quantityinteger✅ SíCantidad vendida (entero positivo)
unit_price_centsinteger✅ SíPrecio unitario en centavos
subtotal_centsinteger✅ SíSubtotal antes de descuento en centavos
net_total_centsinteger✅ SíTotal neto después de descuento en centavos
category_typestring✅ SíClasificación del producto (ver tabla abajo)
waiter_namestring⭐ RecomendadoNombre del mesero — alimenta Star Employees en el dashboard
item_identifieruuidNoUUID del item en RC — permite cruce con recetario
item_skustringNoSKU del producto en RC
categorystringNoNombre de la categoría (ej. “Cortes”, “Cocteles”)
discount_centsintegerNoDescuento aplicado en centavos (default: 0)
Valores válidos para category_type:
ValorDescripciónAlimenta en el dashboard
foodAlimentosFood Cost %, Top 10, Ventas por categoría
beverageBebidas sin alcoholBeverage Cost %, Top 10
barBebidas con alcoholBar Cost %, Top 10
otherSin clasificaciónTop 10
waiter_name es clave para el ranking de meseros (Star Employees) en el dashboard del dueño. Sin este campo, el módulo de Star Employees no tendrá datos. Envíalo en cada línea con el nombre completo del mesero que tomó la orden.
Usa el identifier del Item del POS como item_identifier. Esto permite que Restaurantero Pro cruce con el recetario si lo registraste.

3.5 Descuentos — POST /api/v1/ingest/discounts

Registra los descuentos y cortesías aplicados durante el período. Mapea el modelo Discount de RC. Body del request:
{
  "data": {
    "type": "discounts",
    "attributes": {
      "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
      "date": "2026-05-15",
      "closing_type": "zout",
      "discounts": [
        {
          "discount_identifier": "disc-uuid-1",
          "type": "percentage",
          "factor_type": "percentage",
          "name": "Descuento Empleado",
          "rate": 50,
          "amount_cents": 18500,
          "applied_to": "order",
          "applied_by": "María González",
          "authorized_by": "Roberto Pérez",
          "reason": "Descuento a empleado"
        },
        {
          "discount_identifier": "disc-uuid-2",
          "type": "courtesy",
          "factor_type": "courtesy",
          "name": "Cortesía Gerencia",
          "rate": 100,
          "amount_cents": 43000,
          "applied_to": "order",
          "applied_by": "Roberto Pérez",
          "authorized_by": "Roberto Pérez",
          "reason": "Invitación a cliente VIP"
        }
      ]
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "sent_at": "2026-05-15T23:45:00Z"
    }
  }
}
Tipos de descuento:
type / factor_typeDescripción
percentagePorcentaje sobre el subtotal
fixedMonto fijo en centavos
courtesyCortesía — 100% del monto

3.6 Cancelaciones — POST /api/v1/ingest/cancellations

Registra cancelaciones de órdenes y líneas (voids). Mapea el modelo Cancellation de RC. Las cancelaciones excesivas disparan alertas automáticas en el dashboard del operador. Body del request:
{
  "data": {
    "type": "cancellations",
    "attributes": {
      "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
      "date": "2026-05-15",
      "closing_type": "zout",
      "cancellations": [
        {
          "cancellation_identifier": "canc-uuid-1",
          "item_name": "Filete al Gusto",
          "item_sku": "FILETE-02",
          "quantity": 1,
          "amount_cents": 38500,
          "reason": "Error de captura",
          "cancelled_by": "Luis Herrera",
          "authorized_by": "Roberto Pérez",
          "cancelled_at": "2026-05-15T21:30:00-07:00"
        },
        {
          "cancellation_identifier": "canc-uuid-2",
          "item_name": "Corona Extra",
          "item_sku": "CORONA-355",
          "quantity": 2,
          "amount_cents": 9000,
          "reason": "Cliente cambió de opinión",
          "cancelled_by": "Ana García",
          "authorized_by": "Roberto Pérez",
          "cancelled_at": "2026-05-15T20:15:00-07:00"
        }
      ]
    },
    "meta": {
      "pos_name": "restaurant_controller",
      "sent_at": "2026-05-15T23:45:00Z"
    }
  }
}

4. Endpoints de Inteligencia

Estos son los endpoints que consumes para mostrar datos procesados. Los puedes integrar en el dashboard del POS, en la app del dueño, o en cualquier superficie.
Todos requieren X-API-Key y los parámetros branch_identifier, date_from, date_to.

4.1 Resumen de ventas

GET /api/v1/intelligence/sales/summary
Parámetros:
branch_identifier=uuid
date_from=2026-05-01
date_to=2026-05-15
Response:
{
  "data": {
    "type": "sales_summary",
    "attributes": {
      "total_sales_cents": 8234500,
      "total_sales_no_iva_cents": 7098707,
      "food_sales_cents": 5648741,
      "beverage_sales_cents": 2585759,
      "bar_sales_cents": 0,
      "covers_total": 1341,
      "tables_served_total": 372,
      "avg_ticket_cents": 6143,
      "avg_pax": 3.6,
      "vs_previous_period": {
        "total_sales_pct_change": 12.4,
        "covers_pct_change": 8.1,
        "avg_ticket_pct_change": 3.9
      },
      "by_shift": {
        "breakfast": { "sales_cents": 0,       "covers": 0   },
        "lunch":     { "sales_cents": 5234500,  "covers": 854 },
        "dinner":    { "sales_cents": 3000000,  "covers": 487 },
        "bar":       { "sales_cents": 0,        "covers": 0   }
      }
    },
    "meta": {
      "period": { "from": "2026-05-01", "to": "2026-05-15" },
      "branch_identifier": "uuid"
    }
  }
}

4.2 Ventas por hora

GET /api/v1/intelligence/sales/by-hour
Útil para mapas de calor de ocupación y optimización de staffing. Response:
{
  "data": {
    "type": "sales_by_hour",
    "attributes": {
      "peak_hour": "14:00",
      "valley_hour": "10:00",
      "hours": [
        { "hour": "13:00", "sales_cents": 824500,  "covers": 134, "tables": 38 },
        { "hour": "14:00", "sales_cents": 1234000, "covers": 201, "tables": 56 },
        { "hour": "15:00", "sales_cents": 982000,  "covers": 159, "tables": 44 }
      ]
    }
  }
}

4.3 Ventas por empleado

GET /api/v1/intelligence/sales/by-employee
Ranking de desempeño de meseros. Star Employees.
Este endpoint requiere que hayas enviado waiter_name en las líneas de orden vía POST /api/v1/ingest/orders.
Response:
{
  "data": {
    "type": "sales_by_employee",
    "attributes": {
      "employees": [
        {
          "name": "Ana García",
          "total_sales_cents": 1823400,
          "covers": 298,
          "avg_ticket_cents": 6117,
          "tips_cents": 182340,
          "tip_pct": 10.0,
          "rank": 1
        },
        {
          "name": "Luis Herrera",
          "total_sales_cents": 1456800,
          "covers": 241,
          "avg_ticket_cents": 6045,
          "tips_cents": 130000,
          "tip_pct": 8.9,
          "rank": 2
        }
      ]
    }
  }
}

4.4 Reconciliación de caja

GET /api/v1/intelligence/sales/reconciliation
Tabla detallada de cada corte con efectivo, tarjeta, propinas, y faltante/sobrante. Exportable a PDF. Response:
{
  "data": {
    "type": "reconciliation",
    "attributes": {
      "totals": {
        "cash_sales_cents": 1850000,
        "card_sales_cents": 6384500,
        "tips_cents": 423000,
        "grand_total_cents": 8234500,
        "variance_cents": 3140
      },
      "records": [
        {
          "date": "2026-05-15",
          "shift": "dinner",
          "folio": "Z-00045",
          "cashier": "Ana García",
          "cash_cents": 125000,
          "card_cents": 409550,
          "tips_cents": 30000,
          "total_cents": 534550,
          "variance_cents": 314,
          "variance_status": "green"
        }
      ]
    }
  }
}

4.5 Estado de Cuenta de Efectivo (Cash Flow)

GET /api/v1/intelligence/cash-flow
El libro de caja digital del restaurante. Muestra todas las entradas y salidas de efectivo del período, con saldo inicial y saldo final. Equivale al control de efectivo que antes se hacía en Excel. Response:
{
  "data": {
    "type": "cash_flow",
    "attributes": {
      "period_summary": {
        "opening_balance_cents": 500000,
        "total_inflows_cents": 8734500,
        "total_outflows_cents": 7284200,
        "closing_balance_cents": 1950300,
        "net_cash_cents": 1450300
      },
      "inflows": {
        "cash_sales_cents": 1850000,
        "deposits_received_cents": 100000,
        "other_income_cents": 0,
        "tips_received_cents": 423000
      },
      "outflows": {
        "cash_purchases_cents": 485000,
        "cash_expenses_cents": 342000,
        "tips_paid_to_waiters_cents": 380000,
        "withholding_cents": 75000,
        "other_cash_out_cents": 0
      },
      "daily_entries": [
        {
          "date": "2026-05-15",
          "opening_balance_cents": 500000,
          "inflows_cents": 534550,
          "outflows_cents": 67000,
          "closing_balance_cents": 967550,
          "variance_cents": 314,
          "variance_pct": 0.06,
          "variance_status": "green"
        }
      ],
      "alerts": [
        {
          "date": "2026-05-08",
          "type": "variance_high",
          "message": "Faltante de caja de $850 MXN detectado",
          "severity": "yellow"
        }
      ]
    },
    "meta": {
      "period": { "from": "2026-05-01", "to": "2026-05-15" },
      "benchmark": {
        "variance_pct_threshold": 1.0,
        "variance_status": "green"
      }
    }
  }
}

4.6 Estado de Resultados (P&L)

GET /api/v1/intelligence/pnl
El corazón del producto. Estado de Resultados completo del período, calculado cruzando ventas + compras + nómina capturadas en el dashboard. Response:
{
  "data": {
    "type": "pnl_summary",
    "attributes": {
      "ventas": {
        "food_sales_cents": 5648741,
        "beverage_sales_cents": 2585759,
        "bar_sales_cents": 0,
        "total_net_sales_cents": 8234500,
        "food_sales_pct": 68.6,
        "beverage_sales_pct": 31.4
      },
      "costo_de_ventas": {
        "food_cost_cents": 1694622,
        "food_cost_pct": 30.0,
        "food_cost_status": "yellow",
        "beverage_cost_cents": 517152,
        "beverage_cost_pct": 20.0,
        "beverage_cost_status": "green",
        "total_cogs_cents": 2211774
      },
      "utilidad_bruta": {
        "gross_profit_cents": 6022726,
        "gross_margin_pct": 73.1
      },
      "gastos_operativos": {
        "labor_cost_cents": 2470350,
        "labor_cost_pct": 30.0,
        "labor_cost_status": "yellow",
        "prime_cost_cents": 4682124,
        "prime_cost_pct": 56.9,
        "prime_cost_status": "green",
        "rent_cents": 900000,
        "utilities_cents": 370000,
        "maintenance_cents": 164000,
        "marketing_cents": 240000,
        "admin_cents": 136000,
        "other_expenses_cents": 136000,
        "total_opex_cents": 4416350
      },
      "ebitda": {
        "ebitda_cents": 1606376,
        "ebitda_pct": 19.5,
        "ebitda_status": "yellow"
      },
      "utilidad_neta": {
        "net_profit_cents": 1446376,
        "net_profit_pct": 17.6,
        "net_profit_status": "green"
      }
    },
    "meta": {
      "period": { "from": "2026-05-01", "to": "2026-05-15" },
      "benchmarks": {
        "food_cost":   { "green": "< 30%", "yellow": "30-38%", "red": "> 38%" },
        "labor_cost":  { "green": "< 30%", "yellow": "30-35%", "red": "> 35%" },
        "prime_cost":  { "green": "< 60%", "yellow": "60-65%", "red": "> 65%" },
        "ebitda":      { "green": "> 20%", "yellow": "10-20%", "red": "< 10%" }
      }
    }
  }
}

4.7 Food Cost & Beverage Cost

GET /api/v1/intelligence/food-cost
Desglose de costos de materia prima por categoría y platillo. Requiere que el operador haya capturado compras en el dashboard. Response:
{
  "data": {
    "type": "food_cost",
    "attributes": {
      "food_cost_pct": 30.0,
      "food_cost_status": "yellow",
      "beverage_cost_pct": 20.0,
      "beverage_cost_status": "green",
      "bar_cost_pct": 0.0,
      "by_category": [
        {
          "category": "Carnes",
          "purchases_cents": 845000,
          "sales_cents": 2350000,
          "cost_pct": 35.9,
          "status": "yellow"
        },
        {
          "category": "Mariscos",
          "purchases_cents": 420000,
          "sales_cents": 1850000,
          "cost_pct": 22.7,
          "status": "green"
        }
      ]
    }
  }
}

4.8 Labor Cost

GET /api/v1/intelligence/labor-cost
Costo de nómina como porcentaje de ventas, por departamento. Requiere que el operador haya capturado nómina en el dashboard. Response:
{
  "data": {
    "type": "labor_cost",
    "attributes": {
      "total_labor_cents": 2470350,
      "labor_cost_pct": 30.0,
      "labor_cost_status": "yellow",
      "by_department": [
        { "department": "Cocina",          "amount_cents": 1050000, "pct_of_sales": 12.8 },
        { "department": "Servicio",        "amount_cents": 870000,  "pct_of_sales": 10.6 },
        { "department": "Administración",  "amount_cents": 350000,  "pct_of_sales": 4.3  },
        { "department": "Bar",             "amount_cents": 200350,  "pct_of_sales": 2.4  }
      ]
    }
  }
}

4.9 KPIs consolidados

GET /api/v1/intelligence/kpis
Todos los KPIs en un solo endpoint con semáforo. Ideal para el widget de resumen del dashboard del POS. Response:
{
  "data": {
    "type": "kpis",
    "attributes": {
      "kpis": [
        { "key": "food_cost_pct",     "label": "Food Cost",     "value": 30.0, "unit": "%",     "status": "yellow", "benchmark_green": "< 30%",   "benchmark_red": "> 38%" },
        { "key": "beverage_cost_pct", "label": "Beverage Cost", "value": 20.0, "unit": "%",     "status": "green",  "benchmark_green": "< 20%",   "benchmark_red": "> 28%" },
        { "key": "labor_cost_pct",    "label": "Labor Cost",    "value": 30.0, "unit": "%",     "status": "yellow", "benchmark_green": "< 30%",   "benchmark_red": "> 35%" },
        { "key": "prime_cost_pct",    "label": "Prime Cost",    "value": 56.9, "unit": "%",     "status": "green",  "benchmark_green": "< 60%",   "benchmark_red": "> 65%" },
        { "key": "ebitda_pct",        "label": "EBITDA",        "value": 19.5, "unit": "%",     "status": "yellow", "benchmark_green": "> 20%",   "benchmark_red": "< 10%" },
        { "key": "net_profit_pct",    "label": "Utilidad Neta", "value": 17.6, "unit": "%",     "status": "green",  "benchmark_green": "> 15%",   "benchmark_red": "< 5%"  },
        { "key": "avg_ticket_cents",  "label": "Ticket Prom.",  "value": 6143, "unit": "cents", "status": "green",  "benchmark_green": "> 28000", "benchmark_red": "< 18000" },
        { "key": "cash_variance_pct", "label": "Varianza Caja", "value": 0.06, "unit": "%",     "status": "green",  "benchmark_green": "< 1%",    "benchmark_red": "> 3%"  }
      ]
    },
    "meta": {
      "period": { "from": "2026-05-01", "to": "2026-05-15" }
    }
  }
}
Semáforo de benchmarks (industria restaurantera México):
KPIVerdeAmarilloRojo
Food Cost %< 30%30–38%> 38%
Beverage Cost %< 20%20–28%> 28%
Bar Cost %< 25%25–30%> 30%
Labor Cost %< 30%30–35%> 35%
Prime Cost %< 60%60–65%> 65%
EBITDA %> 20%10–20%< 10%
Utilidad Neta %> 15%5–15%< 5%
Varianza Caja< 1%1–3%> 3%
Inventario DSI< 7 días7–14 días> 14 días

4.10 Alertas activas

GET /api/v1/intelligence/alerts
Lista priorizada de alertas del período. Response:
{
  "data": {
    "type": "alerts",
    "attributes": {
      "total_count": 3,
      "critical_count": 0,
      "warning_count": 2,
      "info_count": 1,
      "alerts": [
        {
          "id": "alert-uuid-1",
          "severity": "yellow",
          "type": "food_cost_high",
          "title": "Food Cost en zona de atención",
          "message": "Tu Food Cost es 30.0% — está en el límite del rango recomendado (< 30%). Revisa los costos de materia prima de Carnes.",
          "action": "Revisar compras de Carnes en el período",
          "created_at": "2026-05-15T06:00:00Z"
        }
      ]
    }
  }
}
Tipos de alerta:
typeSeveridad típicaDescripción
food_cost_highyellow / redFood cost sobre benchmark
bar_cost_highyellow / redBar cost sobre benchmark
labor_cost_highyellow / redLabor cost sobre benchmark
cash_variance_highyellow / redFaltante/sobrante de caja excesivo
discounts_highyellow / redDescuentos inusuales o excesivos
cancellations_highyellow / redCancelaciones sobre el umbral normal
void_alertyellow / redVoids sospechosos por usuario
low_stockyellowInventario bajo mínimo recomendado
sales_upblueVentas superiores al período anterior
sales_downyellow / redVentas inferiores al período anterior

4.11 Matriz de Menú

GET /api/v1/intelligence/menu/engineering
Clasifica cada platillo en uno de 4 cuadrantes según rentabilidad y popularidad.
Requiere que hayas enviado las líneas de orden vía POST /api/v1/ingest/orders con category_type y item_name correctos.
Response:
{
  "data": {
    "type": "menu_engineering",
    "attributes": {
      "summary": {
        "stars_count": 8,
        "puzzles_count": 4,
        "plowhorses_count": 6,
        "dogs_count": 3
      },
      "items": [
        {
          "item_sku": "ARRACHERA-01",
          "item_name": "Arrachera a las Brasas",
          "category": "Cortes",
          "quantity_sold": 94,
          "popularity_pct": 8.2,
          "unit_price_cents": 34500,
          "unit_cost_cents": 10350,
          "contribution_cents": 24150,
          "total_contribution_cents": 2270100,
          "quadrant": "star",
          "quadrant_label": "Estrella",
          "action": "Proteger — no cambiar precio ni receta"
        }
      ]
    }
  }
}
Cuadrantes de la Matriz de Menú:
CuadranteRentabilidadPopularidadAcción recomendada
Estrella (Star)AltaAltaProteger — no tocar precio ni receta
PuzzleAltaBajaPromover — potencial desaprovechado
Caballo (Plowhorse)BajaAltaReducir costo o mantener por volumen
Perro (Dog)BajaBajaEliminar o rediseñar

4.12 Tiendita Inteligente — Sugerencias de compra

GET /api/v1/intelligence/inventory/suggestions
Lista de compra recomendada para los próximos días, basada en ventas reales + stock disponible + lead time del proveedor. Requiere que el operador tenga inventario capturado en el dashboard. Response:
{
  "data": {
    "type": "purchase_suggestions",
    "attributes": {
      "generated_at": "2026-05-15T06:00:00Z",
      "days_ahead": 3,
      "by_supplier": [
        {
          "supplier_name": "Carnes Premium Hermosillo",
          "items": [
            {
              "item_name": "Arrachera USDA Choice",
              "unit": "kg",
              "current_stock": 4.5,
              "suggested_qty": 18.0,
              "unit_cost_cents": 22000,
              "total_cost_cents": 396000,
              "reason": "Stock para 0.8 días — ventas proyectadas de 14 porciones/día"
            }
          ],
          "subtotal_cents": 396000
        }
      ],
      "total_suggested_cents": 645000
    }
  }
}

5. Módulo Live — Datos en tiempo real

Estos endpoints devuelven el estado actual del turno, no datos históricos. Úsalos para mostrar información en tiempo real en el panel del gerente durante el servicio.

5.1 Ventas del turno en curso

GET /api/v1/live/sales/today
Response:
{
  "data": {
    "type": "live_sales",
    "attributes": {
      "as_of": "2026-05-15T21:30:00-07:00",
      "total_sales_cents": 312500,
      "covers_so_far": 54,
      "tables_so_far": 15,
      "avg_ticket_cents": 5787,
      "vs_same_time_yesterday": {
        "sales_pct_change": 8.3,
        "covers_pct_change": 5.1
      },
      "projected_day_total_cents": 534550
    }
  }
}

5.2 Saldo de caja en tiempo real

GET /api/v1/live/cash/balance
Efectivo esperado en caja en este momento. Útil para alertas de faltante durante el turno, antes del Corte Z. Response:
{
  "data": {
    "type": "live_cash",
    "attributes": {
      "as_of": "2026-05-15T21:30:00-07:00",
      "opening_balance_cents": 500000,
      "cash_sales_so_far_cents": 78500,
      "cash_out_so_far_cents": 12000,
      "expected_balance_cents": 566500,
      "status": "green"
    }
  }
}

5.3 Alertas del turno activo

GET /api/v1/live/alerts/active
Alertas generadas durante el turno actual. El POS puede usarlas para notificaciones en tiempo real al gerente. Response:
{
  "data": {
    "type": "live_alerts",
    "attributes": {
      "alerts": [
        {
          "severity": "yellow",
          "type": "discounts_high",
          "message": "Se han aplicado 4 cortesías hoy — 2 más que el promedio diario",
          "triggered_at": "2026-05-15T20:45:00-07:00"
        }
      ]
    }
  }
}

6. Guía de integración — Restaurant Controller POS

Esta sección es para Carlos (dev de RC). Aquí está el mapa exacto de qué conectar, cuándo y cómo — sin ambigüedades.

6.1 Flujo completo de integración

RESTAURANT CONTROLLER POS                    RESTAURANTERO PRO
─────────────────────────────────────────    ──────────────────────────────────

SETUP (una sola vez por sucursal)
────────────────────────────────
1. Marco crea la cuenta en RP              → Group + Brand + Branch creados
2. Marco genera la API Key                 → rpro_live_xxx
3. Carlos pega la key en Settings del RC   → RC queda configurado

DURANTE EL SERVICIO (en tiempo real)
─────────────────────────────────────
4. Gerente consulta ventas del turno       → GET /api/v1/live/sales/today
5. Gerente ve alertas del turno            → GET /api/v1/live/alerts/active

AL CERRAR EL CORTE Z (cada noche)
──────────────────────────────────
6.  POST /api/v1/ingest/orders          ← líneas de productos vendidos
7.  POST /api/v1/ingest/discounts       ← descuentos del día
8.  POST /api/v1/ingest/cancellations   ← cancelaciones y voids
9.  POST /api/v1/ingest/payments        ← desglose por método de pago
10. POST /api/v1/ingest/sales           ← resumen de ventas
11. POST /api/v1/ingest/cash-register   ← CORTE Z (dispara procesamiento)

                                    → RP regenera cubos (asíncrono, ~5 seg)
                                    → Webhook: snapshot.completed
                                    → Alertas calculadas automáticamente
                                    → Insight IA del día generado

AL DÍA SIGUIENTE (dashboard del dueño)
───────────────────────────────────────
12. GET /api/v1/intelligence/pnl        ← P&L del día anterior
13. GET /api/v1/intelligence/kpis       ← Semáforo de KPIs
14. GET /api/v1/intelligence/alerts     ← Alertas priorizadas

6.2 Versión Full Dining (restaurant_pos)

Modelo RCQué contiene→ Endpoint RP
CashRegisterOpening (tipo zout)El corte Z completo con summaries[]POST /api/v1/ingest/cash-register
CashRegisterOpeningSummary[]Efectivo, tarjeta, propinas, retenciones, faltanteIncluido en el body del cash-register
Order → OrderTicket → OrderTicketLine[]Productos vendidos, cantidades, preciosPOST /api/v1/ingest/orders
Payment[] (agrupados por PaymentType)Cómo pagaron los clientesPOST /api/v1/ingest/payments
Discount[]Descuentos y cortesías aplicadosPOST /api/v1/ingest/discounts
Cancellation[]Líneas canceladas y voidsPOST /api/v1/ingest/cancellations
Dónde en RC disparar el envío: En el CashRegisterOpeningController.php, ya existe la lógica que detecta cuando el closing type es zout:
$isStoredZOut = $closingCategoryType === 'cash_register.closings.zout';
Justo después de que esta condición se cumple y RC termina su proceso interno, es el momento de disparar los POSTs a Restaurantero Pro. El orden correcto es el que se muestra en la sección 6.1.

6.3 Versión Express (express_pos)

Express funciona igual que Full Dining en cuanto a la integración. La diferencia está en que Express no tiene mesas ni zonas de reservación, pero los modelos de datos son idénticos: CashRegisterOpening, OrderTicketLine, Payment, Discount, Cancellation existen en ambas versiones con la misma estructura. El dispatch del Corte Z en Express también vive en CashRegisterOpeningController.php — misma lógica, mismo punto de integración.

6.4 Mapeo de modelos RC → endpoints

CashRegisterOpening → /api/v1/ingest/cash-register

$closing = $cashRegisterOpening;

$body = [
    'data' => [
        'type' => 'cash-register',
        'attributes' => [
            'branch_identifier' => config('restaurantero.branch_identifier'),
            'closing_type'      => 'zout',
            'start_at'          => $closing->start_at->toIso8601String(),
            'end_at'            => $closing->end_at->toIso8601String(),
            'status'            => 'completed',
            'summaries'         => $closing->summaries->map(fn($s) => [
                'type'         => $this->mapSummaryType($s->type),
                'name'         => $s->name,
                'amount_cents' => (int) ($s->amount * 100),
            ])->toArray(),
        ],
        'meta' => [
            'pos_name'    => 'restaurant_controller',
            'pos_version' => config('app.version'),
            'pos_variant' => 'full_dining',
            'sent_at'     => now()->toIso8601String(),
            'cashier_name' => $closing->user?->name,
        ],
    ],
];
Mapeo de tipos de summary RC → RP:
Tipo en RCTipo en Restaurantero Pro
Ventas Efectivo / cashcash_sales
Ventas Tarjeta / cardcard_sales
Ventas Tarjeta Créditocard_credit_sales
Ventas Tarjeta Débitocard_debit_sales
Transferenciatransfer_sales
Propinas Cobradastips_received
Propinas Pagadas Mesero (tipPaidToWaiter)tips_paid_to_waiter
Retención / Moche (withholdingAmount)withholding
Depósitos (deposits)deposits
Sobrante / Faltantecash_variance

OrderTicketLine → /api/v1/ingest/orders

$lines = OrderTicketLine::query()
    ->whereHas('ticket.order', fn($q) => $q->whereIn('status', ['closed', 'to_pay']))
    ->whereDate('created_at', today())
    ->with(['ticket.order.user', 'ticket.order.waiter', 'item.category'])
    ->get()
    ->map(fn($line) => [
        'item_identifier'  => $line->item?->identifier,
        'item_sku'         => $line->identification ?? $line->item?->sku,
        'item_name'        => $line->description,
        'category'         => $line->item?->category?->name,
        'category_type'    => $this->mapCategoryType($line->item?->category),
        // ⭐ waiter_name — REQUERIDO para Star Employees en el dashboard
        'waiter_name'      => $line->ticket?->order?->waiter?->name
                           ?? $line->ticket?->order?->user?->name
                           ?? null,
        'quantity'         => (int) ($line->quantity * 100),
        'unit_price_cents' => $line->unit_price,
        'subtotal_cents'   => (int) ($line->subtotal * 100),
        'discount_cents'   => (int) ($line->discount * 100),
        'net_total_cents'  => (int) ($line->netSubtotal * 100),
    ])->toArray();
Los atributos unit_price y quantity en OrderTicketLine de RC usan mutadores que dividen el valor almacenado entre 100 antes de devolverlo. Si en BD está 1500000, el mutador devuelve 150.00. Para enviar a Restaurantero Pro en centavos, multiplica por 100.
waiter_name es clave para Star Employees en el dashboard. El campo waiter en la orden es el mesero asignado. Si no existe waiter, usar user como fallback.

Payment → /api/v1/ingest/payments

$payments = Payment::query()
    ->whereDate('created_at', today())
    ->whereIn('status', ['active', 'paid'])
    ->with('type')
    ->get()
    ->groupBy('type.name')
    ->map(fn($group, $typeName) => [
        'payment_type'      => $this->mapPaymentType($typeName),
        'payment_type_name' => $typeName,
        'amount_cents'      => (int) ($group->sum('amount') * 100),
        'tips_cents'        => 0,
        'change_cents'      => (int) ($group->sum('change') * 100),
        'transaction_count' => $group->count(),
    ])->values()->toArray();

Discount → /api/v1/ingest/discounts

$discounts = Discount::query()
    ->whereDate('created_at', today())
    ->whereIn('status', ['active'])
    ->with(['user', 'category'])
    ->get()
    ->map(fn($d) => [
        'discount_identifier' => $d->identifier,
        'type'                => $d->type,
        'factor_type'         => $d->factor_type,
        'name'                => $d->name,
        'rate'                => $d->rate_or_fee,
        'amount_cents'        => (int) ($d->rate_or_fee * 100),
        'applied_to'          => class_basename($d->discountable_type),
        'applied_by'          => $d->user?->name,
        'authorized_by'       => $d->user?->name,
        'reason'              => $d->description,
    ])->toArray();

Cancellation → /api/v1/ingest/cancellations

$cancellations = Cancellation::query()
    ->whereDate('created_at', today())
    ->with(['cancelledBy', 'authorizedBy', 'cancellationable'])
    ->get()
    ->map(fn($c) => [
        'cancellation_identifier' => $c->identifier,
        'item_name'               => $c->cancellationable?->description ?? 'Sin nombre',
        'item_sku'                => $c->cancellationable?->identification,
        'quantity'                => 1,
        'amount_cents'            => (int) (($c->cancellationable?->subtotal ?? 0) * 100),
        'reason'                  => $c->reason,
        'cancelled_by'            => $c->cancelledBy?->name,
        'authorized_by'           => $c->authorizedBy?->name,
        'cancelled_at'            => $c->created_at->toIso8601String(),
    ])->toArray();

6.5 Cuándo disparar cada POST

Evento en RCQué enviarNotas
CashRegisterOpening status → completed AND tipo = zoutTodos los 6 POSTs en ordenEl trigger principal del día
CashRegisterOpening status → completed AND tipo = xoutSolo POST /ingest/cash-register con closing_type: "xout"Informativo, no dispara cubos
En tiempo real, cada 5-10 min (opcional)GET /api/v1/live/sales/todayPara mostrar ventas del día en panel del gerente
Al cargar el dashboard del dueñoGET /api/v1/intelligence/kpis + GET /api/v1/intelligence/alertsMuestra el resumen del día anterior

6.6 Manejo de idempotencia

Paso 1: El CashRegisterOpening ya tiene un identifier UUID — úsalo directamente:
$idempotencyKey = $cashRegisterOpening->identifier;
// Ejemplo: "550e8400-e29b-41d4-a716-446655440000"
Paso 2: Incluye el mismo UUID en todos los 6 POSTs del mismo Corte Z con sufijo:
'X-Idempotency-Key' => $cashRegisterOpening->identifier,               // cash-register
'X-Idempotency-Key' => $cashRegisterOpening->identifier . '-orders',
'X-Idempotency-Key' => $cashRegisterOpening->identifier . '-payments',
'X-Idempotency-Key' => $cashRegisterOpening->identifier . '-discounts',
'X-Idempotency-Key' => $cashRegisterOpening->identifier . '-cancellations',
'X-Idempotency-Key' => $cashRegisterOpening->identifier . '-sales',
Paso 3: Si el request falla (timeout, error de red), reintenta con el mismo Idempotency Key. Restaurantero Pro detecta el duplicado y responde 200 sin procesar dos veces. Política de reintentos recomendada:
Intento 1:  Inmediato
Intento 2:  5 segundos después
Intento 3:  30 segundos después
Intento 4:  5 minutos después
Intento 5:  Marcar como pendiente y reintentar al arrancar RC al día siguiente

6.7 Snippet PHP listo para copiar

<?php

namespace App\Services;

use App\Models\CashRegisterOpening;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class RestauranteroProService
{
    private string $baseUrl;
    private string $apiKey;
    private string $branchIdentifier;

    public function __construct()
    {
        $this->baseUrl          = config('restaurantero.base_url', 'https://api.restauranteropro.com/api/v1');
        $this->apiKey           = config('restaurantero.api_key');
        $this->branchIdentifier = config('restaurantero.branch_identifier');
    }

    /**
     * Envía el Corte Z completo a Restaurantero Pro.
     * Llama a este método justo después de que RC procesa el cierre del día.
     */
    public function sendZOut(CashRegisterOpening $closing): void
    {
        $closingId = $closing->identifier;

        // 1. Líneas de orden (primero)
        $this->post('/ingest/orders', $this->buildOrdersPayload($closing), $closingId . '-orders');

        // 2. Descuentos
        $this->post('/ingest/discounts', $this->buildDiscountsPayload($closing), $closingId . '-discounts');

        // 3. Cancelaciones
        $this->post('/ingest/cancellations', $this->buildCancellationsPayload($closing), $closingId . '-cancellations');

        // 4. Pagos
        $this->post('/ingest/payments', $this->buildPaymentsPayload($closing), $closingId . '-payments');

        // 5. Resumen de ventas
        $this->post('/ingest/sales', $this->buildSalesPayload($closing), $closingId . '-sales');

        // 6. El corte en sí — este dispara el procesamiento en RP
        $this->post('/ingest/cash-register', $this->buildCashRegisterPayload($closing), $closingId);
    }

    private function buildCashRegisterPayload(CashRegisterOpening $closing): array
    {
        return [
            'data' => [
                'type' => 'cash-register',
                'attributes' => [
                    'branch_identifier' => $this->branchIdentifier,
                    'closing_type'      => 'zout',
                    'start_at'          => $closing->start_at->toIso8601String(),
                    'end_at'            => $closing->end_at->toIso8601String(),
                    'status'            => 'completed',
                    'summaries'         => $closing->summaries->map(fn($s) => [
                        'type'         => $s->type,
                        'name'         => $s->name,
                        'amount_cents' => (int) ($s->amount * 100),
                    ])->toArray(),
                ],
                'meta' => [
                    'pos_name'    => 'restaurant_controller',
                    'pos_version' => config('app.version', '1.0.0'),
                    'pos_variant' => 'full_dining',
                    'sent_at'     => now()->toIso8601String(),
                ],
            ],
        ];
    }

    /**
     * HTTP helper con retry automático (3 intentos).
     */
    private function post(string $endpoint, array $payload, string $idempotencyKey): void
    {
        $attempts    = 0;
        $maxAttempts = 3;
        $delays      = [0, 5, 30];

        while ($attempts < $maxAttempts) {
            try {
                $response = Http::withHeaders([
                    'X-API-Key'         => $this->apiKey,
                    'X-Idempotency-Key' => $idempotencyKey,
                    'Content-Type'      => 'application/json',
                ])->timeout(15)->post($this->baseUrl . $endpoint, $payload);

                if ($response->successful()) {
                    Log::info("RestauranteroPro: {$endpoint} enviado correctamente", [
                        'idempotency_key' => $idempotencyKey,
                        'status'          => $response->status(),
                    ]);
                    return;
                }

                Log::warning("RestauranteroPro: {$endpoint} error HTTP {$response->status()}", [
                    'response' => $response->json(),
                ]);

            } catch (\Exception $e) {
                Log::error("RestauranteroPro: {$endpoint} excepción", [
                    'error'   => $e->getMessage(),
                    'attempt' => $attempts + 1,
                ]);
            }

            $attempts++;
            if ($attempts < $maxAttempts) {
                sleep($delays[$attempts]);
            }
        }

        Log::critical("RestauranteroPro: {$endpoint} falló 3 intentos — idempotency_key: {$idempotencyKey}");
    }
}
Configuración en RC (config/restaurantero.php):
<?php

return [
    'base_url'          => env('RESTAURANTERO_BASE_URL', 'https://api.restauranteropro.com/api/v1'),
    'api_key'           => env('RESTAURANTERO_API_KEY'),
    'branch_identifier' => env('RESTAURANTERO_BRANCH_IDENTIFIER'),
    'enabled'           => env('RESTAURANTERO_ENABLED', true),
];
Variables de entorno en RC (.env):
RESTAURANTERO_BASE_URL=https://api.restauranteropro.com/api/v1
RESTAURANTERO_API_KEY=rpro_test_xxxxxxxxxxxxxxxxxxxxxxxx
RESTAURANTERO_BRANCH_IDENTIFIER=019e0c31-f390-7185-8bff-f5046c392930
RESTAURANTERO_ENABLED=true
Usa rpro_test_xxx durante las pruebas y rpro_live_xxx cuando el cliente esté listo para producción. La URL base es la misma para ambas keys.

6.8 Checklist de integración

Antes de declarar la integración lista para producción, verifica estos 13 puntos:
  • 1. API Key configuradaRESTAURANTERO_API_KEY en .env de RC con el prefijo rpro_live_
  • 2. Branch Identifier correcto — UUID de la sucursal en RESTAURANTERO_BRANCH_IDENTIFIER
  • 3. Corte Z detectado — El service se invoca cuando CashRegisterOpening es tipo zout y status completed
  • 4. Corte X no envía datos de negocio — Solo POST /ingest/cash-register con closing_type: "xout", sin orders/payments/etc.
  • 5. Idempotency Keys únicos — Se usa el identifier del CashRegisterOpening como base con sufijos por endpoint
  • 6. Montos en centavos — Verificar que todos los *_cents son integers y no decimales
  • 7. waiter_name incluido en cada línea de orden — Requerido para Star Employees en el dashboard del dueño
  • 8. category_type correcto — Cada línea tiene food, beverage, bar o other según corresponda
  • 9. Retry implementado — El service reintenta al menos 3 veces con backoff
  • 10. Logging activo — Cada POST exitoso y fallido queda registrado en los logs de RC
  • 11. Orden de envío correcto — orders → discounts → cancellations → payments → sales → cash-register
  • 12. Prueba con key rpro_test_ — Enviar 3 cortes Z de prueba con API Key rpro_test_xxx y verificar que aparecen en el dashboard de RP
  • 13. Activar producción — Cambiar a API Key rpro_live_xxx para datos reales del cliente

7. Webhooks

Restaurantero Pro envía webhooks a tu sistema cuando ocurren eventos importantes. Úsalos para saber cuándo los datos están listos para consultar, sin hacer polling.

Configurar tu webhook

POST /api/v1/developer/webhooks
Content-Type: application/json

{
  "url": "https://tu-pos.com/webhooks/restaurantero",
  "events": ["snapshot.completed", "alert.triggered", "cash.variance"],
  "secret": "tu-secreto-para-verificar-firma"
}

Eventos disponibles

EventoCuándo se disparaÚtil para
snapshot.completedCubos regenerados después de un Corte ZSaber que el P&L y KPIs del día ya están disponibles
alert.triggeredUn KPI cruzó su umbral de alertaNotificación push al dueño o gerente
insight.generatedControllero IA generó el resumen diarioMostrar el insight en el dashboard del POS
cash.varianceFaltante/sobrante excede el umbral configuradoAlerta inmediata al gerente
inventory.low_stockIngrediente bajo el mínimo recomendadoNotificación para comprar
purchase.suggestion_readyTiendita generó nueva lista de compraNotificar al encargado de compras

Estructura del payload

{
  "event": "snapshot.completed",
  "branch_identifier": "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
  "timestamp": "2026-05-15T23:52:00Z",
  "data": {
    "snapshot_type": "daily",
    "date": "2026-05-15",
    "cubes_regenerated": ["sales", "pnl", "cash_ledger", "menu_engineering"]
  }
}

Verificación de firma HMAC-SHA256

Cada webhook incluye el header X-Restaurantero-Signature para verificar que viene de nosotros.
public function handleWebhook(Request $request): Response
{
    $signature = $request->header('X-Restaurantero-Signature');
    $payload   = $request->getContent();
    $secret    = config('restaurantero.webhook_secret');

    $expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);

    if (! hash_equals($expectedSignature, $signature)) {
        return response('Firma inválida', 401);
    }

    $event = $request->json('event');

    match ($event) {
        'snapshot.completed'        => $this->onSnapshotCompleted($request->json()->all()),
        'alert.triggered'           => $this->onAlertTriggered($request->json()->all()),
        'cash.variance'             => $this->onCashVariance($request->json()->all()),
        'purchase.suggestion_ready' => $this->onSuggestionReady($request->json()->all()),
        default                     => null,
    };

    return response('OK', 200);
}

Política de reintentos de webhooks

Si tu endpoint no responde 200 OK, Restaurantero Pro reintenta:
Intento 1: Inmediato
Intento 2: 5 segundos después
Intento 3: 30 segundos después
Intento 4: Marcar como fallido — visible en Dev Portal → Webhook Logs

8. Onboarding de sucursal

Proceso actual

Paso 1 — Marco crea la cuenta en el sistema
Group   → El grupo restaurantero dueño de la suscripción
 └── Brand  → La marca o concepto (ej. "Pitic", "Mariscos El Faro")
       └── Branch → La sucursal física con su RFC, dirección y timezone
Paso 2 — Se genera la API Key para la sucursal Desde Ajustes → Integraciones → API Keys, se genera una key para el branch específico:
rpro_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Paso 3 — Se entrega al dueño El dueño recibe:
  • URL del dashboard: https://api.restauranteropro.com/dashboard
  • Email y contraseña para acceder
  • API Key de su sucursal: rpro_live_xxx...
  • UUID de su sucursal (branch_identifier): el UUID que Carlos necesita en la configuración del RC
Paso 4 — Carlos configura RC
RESTAURANTERO_API_KEY=rpro_live_xxxxxxxxxxxxxxxxxxxxxxxx
RESTAURANTERO_BRANCH_IDENTIFIER=b1c2d3e4-f5a6-7890-abcd-ef1234567890
RESTAURANTERO_ENABLED=true
Paso 5 — Verificar que la conexión funciona
GET /api/v1/ping
X-API-Key: rpro_live_xxx...
Response esperado:
{
  "data": {
    "type": "ping",
    "attributes": {
      "status": "ok",
      "branch_name": "Pitic",
      "plan": "pro",
      "message": "Conexión establecida correctamente con Restaurantero Pro."
    }
  }
}
Paso 6 — Primer Corte Z de prueba
Para el primer corte Z usa la API Key rpro_test_xxx. Los datos irán al servidor de producción pero quedarán identificados como prueba. Coordina con el equipo de Restaurantero Pro para limpiarlos después.

Pruebas de integración

  1. Usa la URL https://api.restauranteropro.com/api/v1 con tu API Key rpro_test_xxx
  2. Envía cortes Z de prueba con datos ficticios
  3. Verifica en el dashboard de Restaurantero Pro que los datos llegaron correctamente
  4. Cuando todo funcione, cambia a tu API Key rpro_live_xxx para producción real

9. Catálogo de errores

Código HTTPCódigo internoMensajeCausaSolución
400RPRO_ERR_400Request malformadoEl JSON no tiene la estructura correctaVerifica la estructura data.type, data.attributes
401RPRO_ERR_401API Key faltante o inválidaHeader X-API-Key ausente o incorrectoVerifica el header y la key en el Dev Portal
403RPRO_ERR_403Sin permisos para este endpointEl plan no incluye este móduloVerifica el plan contratado
404RPRO_ERR_404Branch no encontradobranch_identifier no existe en el sistemaVerifica el UUID del branch
409RPRO_ERR_409Idempotency key ya procesadaEl request ya fue recibido y procesadoNo reintentar — el dato ya está guardado
422RPRO_ERR_422Error de validaciónCampo requerido faltante o valor inválidoLee el campo error.field para saber cuál falló
422RPRO_ERR_422_CENTSMonto no es enteroSe envió un decimal en un campo _centsMultiplica por 100 y convierte a int
429RPRO_ERR_429Rate limit excedidoDemasiados requests por minutoEspera Retry-After segundos
500RPRO_ERR_500Error internoFalla en el servidor de RPReportar a dev@restauranteropro.com con el X-Request-Id
503RPRO_ERR_503Servicio temporalmente no disponibleMantenimiento o sobrecargaReintentar con backoff. Ver status.restauranteropro.com
Ejemplo de respuesta de error 422:
{
  "error": {
    "code": "RPRO_ERR_422",
    "message": "El campo total_sales_cents es requerido y debe ser un entero positivo.",
    "field": "data.attributes.total_sales_cents",
    "received_value": 534.50,
    "request_id": "req_9f3k2m1x",
    "docs": "https://docs.restauranteropro.com/errors#RPRO_ERR_422"
  }
}

10. Changelog

v1.0.1 — Mayo 2026

  • POST /api/v1/ingest/orders — Agregado campo waiter_name por línea (requerido para Star Employees). Cada línea ahora genera una fila independiente en BD para queries directas.
  • Checklist de integración — Actualizado a 13 puntos, agregados verificaciones de waiter_name y category_type.
  • Sección 6.4 — Mapeo de OrderTicketLine actualizado con waiter_name y relaciones correctas.

v1.0.0 — Mayo 2026

Release inicial de la API pública de Restaurantero Pro. Endpoints de ingesta:
  • POST /api/v1/ingest/cash-register
  • POST /api/v1/ingest/sales
  • POST /api/v1/ingest/payments
  • POST /api/v1/ingest/orders
  • POST /api/v1/ingest/discounts
  • POST /api/v1/ingest/cancellations
Endpoints de inteligencia:
  • GET /api/v1/intelligence/sales/summary
  • GET /api/v1/intelligence/sales/by-hour
  • GET /api/v1/intelligence/sales/by-employee
  • GET /api/v1/intelligence/sales/reconciliation
  • GET /api/v1/intelligence/cash-flow
  • GET /api/v1/intelligence/pnl
  • GET /api/v1/intelligence/food-cost
  • GET /api/v1/intelligence/labor-cost
  • GET /api/v1/intelligence/kpis
  • GET /api/v1/intelligence/alerts
  • GET /api/v1/intelligence/menu/engineering
  • GET /api/v1/intelligence/inventory/suggestions
Módulo Live:
  • GET /api/v1/live/sales/today
  • GET /api/v1/live/cash/balance
  • GET /api/v1/live/alerts/active
Webhooks: snapshot.completed, alert.triggered, insight.generated, cash.variance, inventory.low_stock, purchase.suggestion_ready
Restaurantero Pro — API Documentation v1.0.1 Desarrollado por Restaurant Controller · Hermosillo, Sonora, México Mayo 2026 · Confidencial — Solo para partners autorizados