{
  "openapi": "3.0.3",
  "info": {
    "title": "SincBlue Public Booking API",
    "version": "1.0.0",
    "description": "API pública de SincBlue (sincblue.com) para descubrir un negocio y crear reservas: negocio por slug, servicios, especialistas, disponibilidad, clases con cupo, paquetes y creación de reservas. No requiere autenticación; aplica rate limiting por IP (100 req/min general; POST de clientes 20 req/min) y las respuestas incluyen headers de límite. Los endpoints del panel del negocio (privados, con sesión) no forman parte de esta superficie. Soporte: info@sincblue.com.",
    "contact": { "name": "SincBlue", "email": "info@sincblue.com", "url": "https://sincblue.com/developers" },
    "termsOfService": "https://sincblue.com/terms"
  },
  "externalDocs": { "description": "SincBlue para desarrolladores", "url": "https://sincblue.com/developers" },
  "servers": [
    { "url": "https://api.sincblue.com", "description": "API directa" },
    { "url": "https://sincblue.com", "description": "Vía el dominio principal (proxy al API)" }
  ],
  "tags": [
    { "name": "negocio", "description": "Descubrimiento del negocio público" },
    { "name": "disponibilidad", "description": "Servicios, especialistas y horarios disponibles" },
    { "name": "reservas", "description": "Creación de clientes y reservas" },
    { "name": "clases", "description": "Clases con cupo y paquetes" }
  ],
  "paths": {
    "/api/tenants/slug/{slug}": {
      "get": {
        "operationId": "getBusinessBySlug",
        "tags": ["negocio"],
        "summary": "Obtener un negocio público por su slug",
        "description": "Resuelve el negocio (tenant) a partir del slug de su página pública de reservas (sincblue.com/book/{slug} o {slug}.sincblue.com). Es el punto de entrada: el tenantId que devuelve se usa en el resto de los endpoints.",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Slug público del negocio, p. ej. `hadiga`.", "example": "hadiga" }
        ],
        "responses": {
          "200": { "description": "Negocio encontrado.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicBusiness" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/services": {
      "get": {
        "operationId": "listServices",
        "tags": ["disponibilidad"],
        "summary": "Listar los servicios del negocio",
        "description": "Servicios activos que el negocio ofrece (corte, clase, consulta…), con duración y precio. Usa `durationMinutes` y `serviceId` para pedir disponibilidad.",
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": {
          "200": { "description": "Lista de servicios.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/resources": {
      "get": {
        "operationId": "listResources",
        "tags": ["disponibilidad"],
        "summary": "Listar especialistas y recursos reservables",
        "description": "Personas, espacios o equipos que atienden las reservas. `serviceIds` indica qué servicios atiende cada uno; `bookingMode` indica si sus horarios son dinámicos (DYNAMIC) o de agenda fija (FIXED_GRID).",
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": {
          "200": { "description": "Lista de recursos.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Resource" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/availability": {
      "get": {
        "operationId": "getAvailability",
        "tags": ["disponibilidad"],
        "summary": "Horarios disponibles para un servicio en un rango",
        "description": "Slots libres para agendar un servicio entre `from` y `to`. Si se pasa `resourceId`, restringe a ese especialista. Las fechas van en ISO 8601 con offset y respetan la zona horaria del negocio (`timezone` del negocio).",
        "parameters": [
          { "$ref": "#/components/parameters/TenantId" },
          { "name": "serviceId", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Servicio a agendar." },
          { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "date-time" }, "description": "Inicio del rango, ISO 8601 con offset." },
          { "name": "to", "in": "query", "required": true, "schema": { "type": "string", "format": "date-time" }, "description": "Fin del rango, ISO 8601 con offset." },
          { "name": "resourceId", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Limitar a un especialista concreto." }
        ],
        "responses": {
          "200": { "description": "Slots disponibles.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AvailabilityResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/availability-range": {
      "get": {
        "operationId": "getAvailabilityRange",
        "tags": ["disponibilidad"],
        "summary": "Disponibilidad de 7 días agrupada por fecha",
        "description": "Vista de una semana a partir de `from`: mapa de fecha (YYYY-MM-DD) a los horarios disponibles de ese día, cada uno con los especialistas (`resourceIds`) que pueden atenderlo.",
        "parameters": [
          { "$ref": "#/components/parameters/TenantId" },
          { "name": "serviceId", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "Servicio a agendar." },
          { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "date-time" }, "description": "Inicio de la semana, ISO 8601 con offset." }
        ],
        "responses": {
          "200": {
            "description": "Mapa de fecha a horarios disponibles.",
            "content": { "application/json": { "schema": { "type": "object", "additionalProperties": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilityRangeEntry" } }, "description": "Claves = fechas YYYY-MM-DD." } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/customers": {
      "post": {
        "operationId": "upsertCustomer",
        "tags": ["reservas"],
        "summary": "Crear (o reutilizar) el cliente que va a reservar",
        "description": "Alta de cliente previa a la reserva. El negocio hace upsert por teléfono dentro de su ámbito: si el teléfono ya existe, se reutiliza y actualiza esa ficha. Rate limit estricto: 20 req/min por IP.",
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CustomerUpsertRequest" } } }
        },
        "responses": {
          "200": { "description": "Cliente creado o actualizado.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Customer" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/bookings": {
      "post": {
        "operationId": "createBooking",
        "tags": ["reservas"],
        "summary": "Crear una reserva",
        "description": "Crea la reserva para un cliente ya creado con `upsertCustomer`. Dos variantes según el `bookingMode` del especialista: con `slotId` (agenda fija, slots de `FIXED_GRID`) o con `resourceId` + `startTime` (agenda dinámica `DYNAMIC`: cualquier inicio devuelto por `getAvailability`). Si el negocio exige anticipo, la reserva nace pendiente de pago y el flujo de depósito continúa en la página pública del negocio.",
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/CreateBookingSlotRequest" },
                  { "$ref": "#/components/schemas/CreateBookingDynamicRequest" }
                ]
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Reserva creada.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "Conflicto: el horario ya no está disponible.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/class-sessions/public": {
      "get": {
        "operationId": "listPublicClassSessions",
        "tags": ["clases"],
        "summary": "Listar clases con cupo (estudios de yoga, pilates, danza…)",
        "description": "Sesiones de clase públicas del negocio con cupo disponible en el rango pedido. Para negocios de clases (estudios), reservar significa inscribirse a una sesión.",
        "parameters": [
          { "$ref": "#/components/parameters/TenantId" },
          { "name": "from", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Inicio del rango, ISO 8601 con offset." },
          { "name": "to", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Fin del rango, ISO 8601 con offset." }
        ],
        "responses": {
          "200": { "description": "Clases públicas.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PublicClassSession" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{tenantId}/package-products/public": {
      "get": {
        "operationId": "listPublicPackages",
        "tags": ["clases"],
        "summary": "Listar paquetes de sesiones a la venta",
        "description": "Paquetes públicos del negocio (créditos de clases o acceso ilimitado por periodo) con precio y vigencia.",
        "parameters": [{ "$ref": "#/components/parameters/TenantId" }],
        "responses": {
          "200": { "description": "Paquetes públicos.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PublicPackageProduct" } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/tenants/{slug}/coupons/validate": {
      "get": {
        "operationId": "validateCoupon",
        "tags": ["reservas"],
        "summary": "Validar un cupón de descuento",
        "description": "Comprueba si un código de cupón aplica para el negocio, opcionalmente para un cliente y precio base concretos, y estima el descuento.",
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Slug público del negocio." },
          { "name": "code", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Código del cupón." },
          { "name": "customerId", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "Cliente que lo usaría (para reglas de primera visita)." },
          { "name": "basePrice", "in": "query", "required": false, "schema": { "type": "number" }, "description": "Precio base sobre el que estimar el descuento." }
        ],
        "responses": {
          "200": { "description": "Resultado de la validación.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CouponValidation" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "TenantId": {
        "name": "tenantId",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "format": "uuid" },
        "description": "Id del negocio, obtenido con `getBusinessBySlug`."
      }
    },
    "responses": {
      "BadRequest": { "description": "Parámetros inválidos.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "NotFound": { "description": "El recurso no existe.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "RateLimited": {
        "description": "Demasiadas solicitudes: espera y reintenta. Las respuestas incluyen headers de rate limit para autorregularse.",
        "headers": {
          "Retry-After": { "description": "Segundos a esperar antes de reintentar.", "schema": { "type": "integer" } },
          "X-RateLimit-Remaining": { "description": "Solicitudes restantes en la ventana actual.", "schema": { "type": "integer" } }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } }
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "description": "Error estándar del API.",
        "additionalProperties": true,
        "properties": {
          "status": { "type": "integer", "description": "Código HTTP." },
          "errorCode": { "type": "string", "nullable": true, "description": "Código de error estable, p. ej. RATE_LIMITED." },
          "message": { "type": "string", "description": "Mensaje legible en español." },
          "timestamp": { "type": "string", "format": "date-time", "nullable": true }
        }
      },
      "PublicBusiness": {
        "type": "object",
        "description": "Negocio público. Puede incluir campos adicionales no documentados.",
        "additionalProperties": true,
        "properties": {
          "tenantId": { "type": "string", "format": "uuid" },
          "businessName": { "type": "string", "nullable": true },
          "slug": { "type": "string" },
          "tagline": { "type": "string", "nullable": true },
          "city": { "type": "string", "nullable": true },
          "state": { "type": "string", "nullable": true },
          "country": { "type": "string", "nullable": true },
          "addressLine": { "type": "string", "nullable": true },
          "googleMapsUrl": { "type": "string", "nullable": true },
          "timezone": { "type": "string", "description": "Zona IANA del negocio, p. ej. America/Mexico_City. Todas las horas se interpretan aquí." },
          "logoUrl": { "type": "string", "nullable": true },
          "coverPhotoUrl": { "type": "string", "nullable": true },
          "instagramHandle": { "type": "string", "nullable": true },
          "whatsappNumber": { "type": "string", "nullable": true },
          "currencyCode": { "type": "string", "description": "Moneda de los precios, p. ej. MXN." },
          "timeFormat": { "type": "string", "enum": ["H24", "H12"] },
          "weekStartDay": { "type": "integer", "enum": [0, 1] },
          "bookingSuspended": { "type": "boolean", "description": "Si es true, el negocio no acepta reservas por ahora." },
          "capabilities": { "type": "array", "items": { "type": "string" }, "description": "Módulos activos del negocio, p. ej. APPOINTMENTS, CLASSES, PACKAGES, DEPOSITS." },
          "contactPhone": { "type": "string", "nullable": true },
          "contactEmail": { "type": "string", "nullable": true },
          "bookingPolicies": { "type": "object", "additionalProperties": true, "description": "Políticas de reserva del negocio (anticipos, transferencias…)." }
        },
        "required": ["tenantId", "slug", "timezone"]
      },
      "Service": {
        "type": "object",
        "description": "Servicio que ofrece el negocio.",
        "additionalProperties": true,
        "properties": {
          "serviceId": { "type": "string", "format": "uuid" },
          "tenantId": { "type": "string", "format": "uuid" },
          "title": { "type": "string" },
          "category": { "type": "string", "nullable": true },
          "description": { "type": "string", "nullable": true },
          "durationMinutes": { "type": "integer", "nullable": true },
          "price": { "type": "number", "nullable": true },
          "currency": { "type": "string", "nullable": true },
          "priceStartsFrom": { "type": "boolean", "description": "true si el precio es «desde»." },
          "isGroupAllowed": { "type": "boolean", "description": "true si el servicio se imparte en grupo (clases)." },
          "maxGroupSize": { "type": "integer", "nullable": true },
          "isActive": { "type": "boolean" },
          "photoUrls": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["serviceId", "title", "isActive"]
      },
      "Resource": {
        "type": "object",
        "description": "Especialista, espacio o equipo que atiende reservas.",
        "additionalProperties": true,
        "properties": {
          "resourceId": { "type": "string", "format": "uuid" },
          "tenantId": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "resourceType": { "type": "string", "enum": ["PERSON", "SPACE", "EQUIPMENT"], "nullable": true },
          "description": { "type": "string", "nullable": true },
          "specialty": { "type": "string", "nullable": true },
          "photoUrl": { "type": "string", "nullable": true },
          "capacity": { "type": "integer", "nullable": true },
          "bookingMode": { "type": "string", "enum": ["DYNAMIC", "FIXED_GRID", "MANUAL"], "description": "DYNAMIC: reservar con resourceId+startTime. FIXED_GRID: reservar con slotId." },
          "serviceIds": { "type": "array", "items": { "type": "string", "format": "uuid" }, "description": "Servicios que atiende." },
          "isActive": { "type": "boolean" }
        },
        "required": ["resourceId", "name", "bookingMode", "serviceIds", "isActive"]
      },
      "AvailabilityResponse": {
        "type": "object",
        "properties": {
          "slots": { "type": "array", "items": { "$ref": "#/components/schemas/AvailabilitySlot" } }
        },
        "required": ["slots"]
      },
      "AvailabilitySlot": {
        "type": "object",
        "description": "Horario disponible para agendar.",
        "properties": {
          "startTime": { "type": "string", "format": "date-time", "description": "ISO 8601 con offset." },
          "endTime": { "type": "string", "format": "date-time" },
          "price": { "type": "number", "nullable": true }
        },
        "required": ["startTime", "endTime"]
      },
      "AvailabilityRangeEntry": {
        "type": "object",
        "description": "Horario disponible de un día, con los especialistas que pueden atenderlo.",
        "properties": {
          "startTime": { "type": "string", "format": "date-time" },
          "endTime": { "type": "string", "format": "date-time" },
          "resourceIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
          "price": { "type": "number", "nullable": true }
        },
        "required": ["startTime", "endTime", "resourceIds"]
      },
      "CustomerUpsertRequest": {
        "type": "object",
        "description": "Datos del cliente que reserva. El teléfono es la clave de upsert dentro del negocio.",
        "properties": {
          "fullName": { "type": "string" },
          "nickname": { "type": "string" },
          "phone": { "type": "string", "description": "Teléfono del cliente (clave de upsert)." },
          "email": { "type": "string", "format": "email" },
          "notes": { "type": "string" },
          "communicationPref": { "type": "string", "enum": ["WHATSAPP", "SMS", "EMAIL", "PHONE", "NONE"] },
          "waOptIn": { "type": "boolean", "description": "Consentimiento para recordatorios por WhatsApp." },
          "referralCode": { "type": "string" }
        },
        "required": ["fullName", "phone"]
      },
      "Customer": {
        "type": "object",
        "description": "Cliente del negocio. Puede incluir campos adicionales.",
        "additionalProperties": true,
        "properties": {
          "customerId": { "type": "string", "format": "uuid" },
          "tenantId": { "type": "string", "format": "uuid" },
          "fullName": { "type": "string" },
          "phone": { "type": "string", "nullable": true },
          "email": { "type": "string", "nullable": true },
          "isActive": { "type": "boolean" },
          "createdAt": { "type": "string" }
        },
        "required": ["customerId", "tenantId", "fullName"]
      },
      "CreateBookingSlotRequest": {
        "type": "object",
        "title": "Reserva sobre slot fijo (FIXED_GRID)",
        "description": "Para especialistas con bookingMode FIXED_GRID: reservar un slot publicado.",
        "properties": {
          "slotId": { "type": "string", "format": "uuid" },
          "customerId": { "type": "string", "format": "uuid", "description": "De upsertCustomer." },
          "serviceId": { "type": "string", "format": "uuid" },
          "bookingSource": { "type": "string", "enum": ["APP", "WEBSITE"], "description": "Origen de la reserva." },
          "bookingNotes": { "type": "string" },
          "addonServiceIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
          "couponCode": { "type": "string" }
        },
        "required": ["slotId", "customerId", "serviceId", "bookingSource"]
      },
      "CreateBookingDynamicRequest": {
        "type": "object",
        "title": "Reserva dinámica (DYNAMIC)",
        "description": "Para especialistas con bookingMode DYNAMIC: reservar un inicio devuelto por getAvailability.",
        "properties": {
          "resourceId": { "type": "string", "format": "uuid" },
          "startTime": { "type": "string", "format": "date-time", "description": "Inicio elegido, ISO 8601 con offset." },
          "customerId": { "type": "string", "format": "uuid", "description": "De upsertCustomer." },
          "serviceId": { "type": "string", "format": "uuid" },
          "bookingSource": { "type": "string", "enum": ["APP", "WEBSITE"] },
          "bookingNotes": { "type": "string" },
          "addonServiceIds": { "type": "array", "items": { "type": "string", "format": "uuid" } },
          "couponCode": { "type": "string" }
        },
        "required": ["resourceId", "startTime", "customerId", "serviceId", "bookingSource"]
      },
      "Booking": {
        "type": "object",
        "description": "Reserva creada. Puede incluir campos adicionales no documentados.",
        "additionalProperties": true,
        "properties": {
          "bookingId": { "type": "string", "format": "uuid" },
          "bookingReference": { "type": "string", "nullable": true },
          "tenantId": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["PENDING", "PENDING_PAYMENT", "CONFIRMED", "COMPLETED", "CANCELLED", "NO_SHOW", "SERIES_CONFLICT"] },
          "startTime": { "type": "string", "format": "date-time", "nullable": true },
          "endTime": { "type": "string", "format": "date-time", "nullable": true },
          "serviceId": { "type": "string", "format": "uuid", "nullable": true },
          "serviceTitle": { "type": "string", "nullable": true },
          "resourceId": { "type": "string", "format": "uuid", "nullable": true },
          "resourceName": { "type": "string", "nullable": true },
          "customerId": { "type": "string", "format": "uuid", "nullable": true },
          "totalAmount": { "type": "number" },
          "currency": { "type": "string", "nullable": true },
          "paymentStatus": { "type": "string", "nullable": true },
          "depositStatus": { "type": "string", "nullable": true, "description": "Si el negocio pide anticipo, estado del depósito." },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["bookingId", "tenantId", "status", "totalAmount", "createdAt"]
      },
      "PublicClassSession": {
        "type": "object",
        "description": "Clase pública con cupo.",
        "properties": {
          "classSessionId": { "type": "string", "format": "uuid" },
          "serviceTitle": { "type": "string" },
          "instructorName": { "type": "string" },
          "start": { "type": "string", "format": "date-time" },
          "end": { "type": "string", "format": "date-time" },
          "capacity": { "type": "integer" },
          "availableSeats": { "type": "integer" },
          "price": { "type": "number", "nullable": true },
          "roomName": { "type": "string", "nullable": true },
          "serviceId": { "type": "string", "format": "uuid", "nullable": true }
        },
        "required": ["classSessionId", "serviceTitle", "instructorName", "start", "end", "capacity", "availableSeats"]
      },
      "PublicPackageProduct": {
        "type": "object",
        "description": "Paquete de sesiones a la venta.",
        "properties": {
          "packageProductId": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "description": { "type": "string", "nullable": true },
          "productType": { "type": "string", "enum": ["CREDITS", "UNLIMITED"] },
          "credits": { "type": "integer", "nullable": true, "description": "Número de clases incluidas (null si UNLIMITED)." },
          "price": { "type": "number" },
          "validityDays": { "type": "integer", "nullable": true },
          "serviceTitles": { "type": "array", "items": { "type": "string" } }
        },
        "required": ["packageProductId", "name", "productType", "price"]
      },
      "CouponValidation": {
        "type": "object",
        "description": "Resultado de validar un cupón.",
        "properties": {
          "valid": { "type": "boolean" },
          "reason": { "type": "string", "nullable": true, "description": "Motivo si no es válido: NOT_FOUND, INACTIVE, EXPIRED, EXHAUSTED, FIRST_VISIT_ONLY…" },
          "discountType": { "type": "string", "enum": ["PERCENTAGE", "FIXED"], "nullable": true },
          "discountValue": { "type": "number", "nullable": true },
          "estimatedDiscount": { "type": "number", "nullable": true }
        },
        "required": ["valid"]
      }
    }
  }
}
