{"openapi":"3.1.0","info":{"title":"ADIPA Coupon Microservice","description":"\n## ADIPA Coupon Management Microservice\n\nWrapper sobre WooCommerce v3 API con lógica de negocio propia.\n\n### Modelo de creación\n\nLa creación es **manual**: se envían `discount_type` (`fixed_amount` | `percent`) y `amount`\nde forma explícita. No hay auto-forzado por tipo.\n\n- `discount_type=percent` → descuento porcentual del carrito.\n- `discount_type=fixed_amount` → monto fijo. Se mapea a WooCommerce como `fixed_product`\n  cuando hay `products`, o `fixed_cart` cuando aplica al carrito completo.\n- `type` es una **etiqueta opcional** (catálogo `coupon_types`) solo para segmentación/métricas.\n- Soporta `products`, `categories`, `excluded_products`, `excluded_categories`, `minimum_expense`,\n  `maximum_expense`, `expires_at`/`expiration_days`.\n\n### Autenticación\n\nTodos los endpoints requieren `X-API-Key` o Bearer JWT Keycloak.\n\n### Reglas de negocio\n\n- **Usage tracking LOCAL** (no depende de WooCommerce).\n- **Email restrictions** locales (workaround bug WC).\n- **Rule Engine** sigue activo para reglas globales del operador (`coupon_type IS NULL`).\n    ","version":"1.0.0"},"paths":{"/api/v1/auth/me":{"get":{"tags":["Health","Auth"],"summary":"Valida credencial (Bearer JWT o X-API-Key) y devuelve el payload","operationId":"auth_me_api_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}]}},"/healthz":{"get":{"tags":["Health"],"summary":"Liveness probe (no deps)","operationId":"liveness_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"parameters":[{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}]}},"/readyz":{"get":{"tags":["Health"],"summary":"Readiness probe — DB + Redis + WC per country","operationId":"readiness_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"parameters":[{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}]}},"/healthz/workers":{"get":{"tags":["Health"],"summary":"Background worker liveness","operationId":"worker_health_healthz_workers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"parameters":[{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}]}},"/api/v1/coupons/bulk":{"post":{"tags":["Coupons"],"summary":"Crear N cupones en bulk","description":"Genera entre 1 y 100 cupones del mismo tipo en una sola llamada. Cada cupón obtiene su propio código único. Rate limit configurable vía `RATE_LIMIT_BULK` (default `5/minute`).","operationId":"bulk_create_coupons_api_v1_coupons_bulk_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"X-User-Email","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Email"}},{"name":"X-Actor-Email","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Actor-Email"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCouponCreate"}}}},"responses":{"201":{"description":"Bulk creado. `meta.total` = número de cupones generados.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validación de reglas falló para uno o más cupones.","content":{"application/json":{"example":{"detail":"quantity must be between 1 and 100"}}}},"429":{"description":"Rate limit excedido."}}}},"/api/v1/coupons/bulk-extend":{"post":{"tags":["Coupons"],"summary":"Cambiar vencimiento de N cupones (batch)","description":"Ajusta la expiración de cada código en `codes` (máx. 200), reutilizando la misma lógica que `PUT /coupons/{code}`. Acepta exactamente uno de `extend_days` (relativo, N días desde AHORA) o `expires_at` (absoluto — fija esa fecha exacta en cada código, y puede acortar la vigencia). Cada ajuste exitoso bumpea versión, escribe `CouponVersion`, y reactiva el cupón si estaba soft-deleted. Los códigos se procesan de forma independiente: un código inexistente o con error no aborta el resto del batch, solo se reporta en `results`.","operationId":"bulk_extend_coupons_api_v1_coupons_bulk_extend_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"X-Actor-Email","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Actor-Email"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkExtendRequest"}}}},"responses":{"200":{"description":"Batch procesado. `data.updated`/`data.failed` resumen el resultado; `data.results` detalla cada código.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons":{"post":{"tags":["Coupons"],"summary":"Crear un cupón","description":"Crea un cupón de forma manual. Campos requeridos: `discount_type` (`fixed_amount` | `percent`) y `amount`.\n\n- `fixed_amount` se mapea a WooCommerce como `fixed_product` si hay `products`, o `fixed_cart` en caso contrario.\n- `type` es opcional (etiqueta del catálogo `coupon_types`), solo para métricas.\n- Soporta `products`, `categories`, `excluded_products`, `excluded_categories`, `minimum_expense`, `maximum_expense`, `expires_at`/`expiration_days`.\n\nEl código se genera automáticamente y se valida único (DB + WC). Rate limit `RATE_LIMIT_CREATE` (default `30/minute`).","operationId":"create_coupon_api_v1_coupons_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"X-User-Email","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-User-Email"}},{"name":"X-Actor-Email","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Actor-Email"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponCreate"}}}},"responses":{"201":{"description":"Cupón creado y sincronizado con WooCommerce.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validación falló (tipo inexistente, productos inválidos, etc.).","content":{"application/json":{"example":{"detail":"el tipo de cupón 'foo' no existe en el catálogo del país"}}}},"429":{"description":"Rate limit excedido."}}},"get":{"tags":["Coupons"],"summary":"Listar cupones (paginado, filtros)","description":"Lista cupones del país (`X-Country`). Filtros opcionales: `type`, `status`, `email`, `code` (búsqueda parcial case-insensitive).","operationId":"list_coupons_api_v1_coupons_get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Per Page"}},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"}},{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/campaign/upsert":{"post":{"tags":["Coupons"],"summary":"Crear o actualizar el cupón de campaña personal del caller (venta nocturna 2026)","description":"Idempotente por email: crea el cupón en la primera llamada, recalcula su monto en llamadas siguientes (ej. cambió el total del carrito guardado), y no lo toca una vez canjeado. Usado por adipa-headless cuando se guarda/actualiza un carrito.","operationId":"upsert_campaign_coupon_api_v1_coupons_campaign_upsert_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignUpsertRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/campaign/mine":{"get":{"tags":["Coupons"],"summary":"Consultar el cupón de campaña vigente del caller (venta nocturna 2026)","description":"404 si el email no tiene cupón de campaña, o ya fue canjeado o expiró. Usado por el plugin de WP para traducir la palabra genérica 'venta-nocturna' al código real del cliente del lado del navegador.","operationId":"get_my_campaign_coupon_api_v1_coupons_campaign_mine_get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","title":"Email"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/referral/reward":{"post":{"tags":["Coupons"],"summary":"Emitir (o recuperar) el cupón de tier del programa de referidos","description":"Idempotente por `idempotency_key`: adipa-headless llama este endpoint una vez por (referidor, tier) desbloqueado, con reintentos ante fallo — dos llamadas con la misma key devuelven el mismo cupón, `created:false` en la segunda. Respuesta sin sobre (`code`/`expires_at`/`created` en el nivel raíz), no `{data: ...}` como el resto de este router, porque el cliente M2M de headless la lee así.","operationId":"emit_referral_reward_api_v1_coupons_referral_reward_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralRewardRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferralRewardResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/mine":{"get":{"tags":["Coupons"],"summary":"Listar mis cupones (sesión Keycloak)","description":"Cupones restringidos al email del usuario autenticado. **Auth:** JWT Keycloak (`Authorization: Bearer <token>`) — usa el claim `email` del token de sesión. Devuelve `[]` si el usuario no tiene cupones asociados.","operationId":"list_my_coupons_api_v1_coupons_mine_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"401":{"description":"Token Bearer ausente o inválido."},"422":{"description":"El token no tiene claim `email`."}}}},"/api/v1/coupons/{code}":{"get":{"tags":["Coupons"],"summary":"Obtener un cupón por código","description":"Devuelve el cupón con su `usage_count` actual. Cache TTL 60s.","operationId":"get_coupon_api_v1_coupons__code__get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Cupón no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Coupons"],"summary":"Actualizar un cupón","description":"Actualiza campos mutables (`amount`, `discount_type`, `usage_limit`, `expires_at`, `email`, `maximum_expense`, `excluded_categories`, `allowed_product_ids`, `individual_use`). Editar un cupón eliminado lo reactiva (limpia `deleted_at`). Cada edición exitosa queda registrada como una versión inmutable nueva (`GET /coupons/{code}/versions`) — envía `change_reason` para documentar el motivo, y el header `X-Actor-Email` para identificar al actor (fallback `api_key`).\n\n**Concurrencia optimista:** incluye `If-Match: <version>` (entero) con la versión que recibiste en el último GET. Si otro proceso modificó el cupón mientras tanto, se devuelve 409 y debes refetch + reintentar.","operationId":"update_coupon_api_v1_coupons__code__put","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"If-Match","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"If-Match"}},{"name":"X-Actor-Email","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Actor-Email"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponUpdate"}}}},"responses":{"200":{"description":"Cupón actualizado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"400":{"description":"`If-Match` no es entero válido."},"404":{"description":"Cupón no encontrado."},"409":{"description":"Conflict — versión esperada no coincide."},"422":{"description":"Validación falló (categoría excluida inexistente, headless no disponible).","content":{"application/json":{"example":{"detail":"categorías excluidas no encontradas: [999]"}}}}}},"delete":{"tags":["Coupons"],"summary":"Eliminar (soft-delete) un cupón","description":"Marca el cupón como eliminado (`deleted_at = now`). El cupón sigue existiendo en DB pero queda invisible para validate/apply. Restaurable vía `POST /admin/coupons/{code}/restore`. No se elimina del WooCommerce.","operationId":"delete_coupon_api_v1_coupons__code__delete","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Cupón soft-deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Cupón no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/{code}/versions":{"get":{"tags":["Coupons"],"summary":"Historial de versiones del cupón","description":"Devuelve el historial completo e inmutable de versiones del cupón (orden descendente), cada una con los campos que cambiaron, el actor, el motivo (`change_reason`) y cuántos `coupon_usages` quedaron fijados a esa versión.","operationId":"get_coupon_versions_api_v1_coupons__code__versions_get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Cupón no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/{code}/usages":{"get":{"tags":["Coupons"],"summary":"Historial de usos (canjes) del cupón","description":"Lista paginada de `coupon_usages` (quién y cuándo canjeó el cupón), orden descendente por fecha de uso. `search` filtra por email u order_id (case-insensitive, contains). Funciona también para cupones soft-deleted — es historial de solo lectura.","operationId":"get_coupon_usages_api_v1_coupons__code__usages_get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Per Page"}},{"name":"search","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Search"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Cupón no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/{code}/validate":{"post":{"tags":["Coupons"],"summary":"Validar cupón sin registrar uso","description":"Verifica si el cupón es aplicable (email, productos, expiración, usage_limit) **sin** consumir un uso. Útil para mostrar feedback en checkout antes del pago.\n\nCache TTL 15s — invalidado en cada apply real.","operationId":"validate_coupon_api_v1_coupons__code__validate_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateCouponRequest"}}}},"responses":{"200":{"description":"Resultado de validación.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"},"examples":{"valido":{"summary":"Cupón válido","value":{"data":{"valid":true,"reasons":[],"coupon":{"code":"BD-ABC123"}}}},"invalido":{"summary":"Cupón inválido","value":{"data":{"valid":false,"reasons":["expired","email_not_allowed"]}}}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupons/{code}/apply":{"post":{"tags":["Coupons"],"summary":"Aplicar cupón (registra uso)","description":"Valida + registra uso del cupón con lock distribuido (Redis) + SELECT FOR UPDATE.\n\n**A diferencia de** `POST /api/v1/webhooks/coupons/{code}/apply`:\n- Respeta `usage_limit` (rechaza si alcanzado).\n- Respeta `expires_at` (rechaza si expirado).\n- NO idempotente — cada call cuenta como uso nuevo.\n\n**Auth:** JWT Keycloak **o** X-API-Key (único endpoint que acepta JWT).\n\nRate limit `RATE_LIMIT_APPLY` (default `60/minute`). Si hay un apply concurrente sobre el mismo cupón, retorna 429 con `Retry-After: 1`.","operationId":"apply_coupon_api_v1_coupons__code__apply_post","security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyCouponRequest"}}}},"responses":{"200":{"description":"Cupón aplicado, uso registrado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Cupón inválido (expirado, límite alcanzado, email no autorizado, etc.)."},"429":{"description":"Apply concurrente en curso o rate limit excedido.","headers":{"Retry-After":{"description":"Segundos a esperar antes de reintentar.","schema":{"type":"string"}}}}}}},"/api/v1/coupon-types":{"post":{"tags":["Coupon Types"],"summary":"Crear un tipo de cupón (catálogo)","description":"Crea una etiqueta de tipo para segmentación/métricas. El `slug` es inmutable.","operationId":"create_coupon_type_api_v1_coupon_types_post","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponTypeCreate"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"409":{"description":"Ya existe un tipo con ese slug en el país."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Coupon Types"],"summary":"Listar tipos de cupón","operationId":"list_coupon_types_api_v1_coupon_types_get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Per Page"}},{"name":"enabled","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/coupon-types/{slug}":{"get":{"tags":["Coupon Types"],"summary":"Obtener un tipo de cupón por slug","operationId":"get_coupon_type_api_v1_coupon_types__slug__get","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Tipo no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Coupon Types"],"summary":"Actualizar un tipo de cupón (name, description, enabled)","operationId":"update_coupon_type_api_v1_coupon_types__slug__put","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CouponTypeUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Tipo no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Coupon Types"],"summary":"Deshabilitar un tipo de cupón (soft-disable)","description":"No elimina físicamente: marca `enabled=false` para preservar historial/métricas.","operationId":"disable_coupon_type_api_v1_coupon_types__slug__delete","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiResponse"}}}},"404":{"description":"Tipo no encontrado."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/webhooks/wc/{event}":{"post":{"tags":["WC Webhooks"],"summary":"Recibir evento entrante de WooCommerce","description":"Endpoint que recibe eventos del plugin WordPress `adipa-coupons-relay`. Verifica firma HMAC y publica el evento en el stream Redis inbound para procesamiento asíncrono por `inbound_consumer`.\n\n**Eventos soportados actualmente:**\n- `coupon.synced` (activo) — sincronización inversa desde WP\n\n**Eventos reservados (sin handler aún):** `order.completed`, `coupon.applied`. Se aceptan y publican al stream pero el worker no los procesa.\n\n\n### Firma HMAC\n\nHeaders requeridos:\n\n| Header | Valor |\n|---|---|\n| `X-Country` | `cl` \\| `mx` \\| `co` |\n| `X-Adipa-Timestamp` | Epoch en segundos. Tolerancia ±5min vs servidor. |\n| `X-Adipa-Signature` | `HMAC-SHA256(hmac_secret, f\"{timestamp}.{raw_body}\")` en hex |\n\n`hmac_secret` se configura por país en `deploy/countries.toml`.\n\n### Ejemplo de firma (Python)\n\n```python\nimport hmac, hashlib, time, json, requests\n\nsecret = \"1971f5d8676802fd\"  # cl dev secret\nbody = json.dumps({\"order_id\": \"ORD-001\", \"external_reference\": \"pi_3PZabc\"}, separators=(',', ':'))\nts = str(int(time.time()))\nsig = hmac.new(secret.encode(), f\"{ts}.{body}\".encode(), hashlib.sha256).hexdigest()\n\nrequests.post(\n    \"http://localhost:8090/api/v1/webhooks/coupons/BD-ABC123/apply\",\n    headers={\n        \"Content-Type\": \"application/json\",\n        \"X-Country\": \"cl\",\n        \"X-Adipa-Timestamp\": ts,\n        \"X-Adipa-Signature\": sig,\n        \"X-Webhook-Source\": \"payment_gateway:stripe\",\n    },\n    data=body,\n)\n```\n\n**Para testing local:** usa el dev endpoint `POST /api/v1/dev/webhook-apply/{code}` que firma y reenvía\nautomáticamente. No disponible en producción.","operationId":"receive_wc_webhook_webhooks_wc__event__post","parameters":[{"name":"event","in":"path","required":true,"schema":{"type":"string","title":"Event"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"204":{"description":"Evento aceptado y publicado al stream."},"401":{"description":"Firma HMAC inválida o timestamp fuera de ventana."},"422":{"description":"Body no es JSON válido."}}}},"/api/v1/webhooks/coupons/{code}/apply":{"post":{"tags":["Coupon Webhooks"],"summary":"Registrar uso de cupón desde pasarela de pago","description":"Endpoint para que pasarelas de pago (Stripe, MercadoPago, Webpay) confirmen el uso de un cupón **después** de procesar el pago. A diferencia de `POST /api/v1/coupons/{code}/apply`, este endpoint:\n\n- **Bypassa `usage_limit`** — si ya se alcanzó, registra igual con `over_limit=true`.\n- **Bypassa `expires_at`** — registra aunque esté expirado.\n- **Es idempotente** por `(order_id, external_reference)` — retry del gateway retorna 200 con el mismo `usage_id` (sin crear duplicado).\n- **Sigue validando** email_restriction y allowed_product_ids (anti-fraude).\n\n### Status codes\n\n| Código | Significado |\n|---|---|\n| 201 | Nuevo uso registrado |\n| 200 | Replay idempotente (mismo external_reference+order_id) |\n| 401 | Firma HMAC inválida o timestamp fuera de ventana |\n| 404 | Cupón no encontrado |\n| 422 | Body inválido o falló validación de email/productos |\n\n### Campo `override_reason`\n\nSolo presente cuando `over_limit=true`. Valores: `usage_limit_exceeded`, `expired`, `both`.\n\n### Firma HMAC\n\nHeaders requeridos:\n\n| Header | Valor |\n|---|---|\n| `X-Country` | `cl` \\| `mx` \\| `co` |\n| `X-Adipa-Timestamp` | Epoch en segundos. Tolerancia ±5min vs servidor. |\n| `X-Adipa-Signature` | `HMAC-SHA256(hmac_secret, f\"{timestamp}.{raw_body}\")` en hex |\n\n`hmac_secret` se configura por país en `deploy/countries.toml`.\n\n### Ejemplo de firma (Python)\n\n```python\nimport hmac, hashlib, time, json, requests\n\nsecret = \"1971f5d8676802fd\"  # cl dev secret\nbody = json.dumps({\"order_id\": \"ORD-001\", \"external_reference\": \"pi_3PZabc\"}, separators=(',', ':'))\nts = str(int(time.time()))\nsig = hmac.new(secret.encode(), f\"{ts}.{body}\".encode(), hashlib.sha256).hexdigest()\n\nrequests.post(\n    \"http://localhost:8090/api/v1/webhooks/coupons/BD-ABC123/apply\",\n    headers={\n        \"Content-Type\": \"application/json\",\n        \"X-Country\": \"cl\",\n        \"X-Adipa-Timestamp\": ts,\n        \"X-Adipa-Signature\": sig,\n        \"X-Webhook-Source\": \"payment_gateway:stripe\",\n    },\n    data=body,\n)\n```\n\n**Para testing local:** usa el dev endpoint `POST /api/v1/dev/webhook-apply/{code}` que firma y reenvía\nautomáticamente. No disponible en producción.","operationId":"webhook_coupon_apply_api_v1_webhooks_coupons__code__apply_post","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Webhook-Source","in":"header","required":false,"schema":{"type":"string","default":"unknown","title":"X-Webhook-Source"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookApplyRequest"}}}},"responses":{"201":{"description":"Nuevo uso registrado.","content":{"application/json":{"schema":{},"example":{"data":{"coupon_code":"BD-ABC123","usage_id":42,"over_limit":false,"usage_count_before":0,"usage_count_after":1,"usage_limit":1,"registered_at":"2026-05-27T17:01:54.964183Z"}}}}},"200":{"description":"Replay idempotente — mismo external_reference+order_id ya registrado.","content":{"application/json":{"example":{"data":{"coupon_code":"BD-ABC123","usage_id":42,"over_limit":false,"usage_count_before":1,"usage_count_after":1,"usage_limit":1,"registered_at":"2026-05-27T17:01:54.964183Z"}}}}},"401":{"description":"Firma HMAC inválida o timestamp fuera de ventana.","content":{"application/json":{"example":{"detail":"Firma HMAC inválida"}}}},"404":{"description":"Cupón no encontrado.","content":{"application/json":{"example":{"detail":"Coupon BD-XYZ not found"}}}},"422":{"description":"Body inválido o validación fallida.","content":{"application/json":{"example":{"detail":"Email no autorizado para este cupón"}}}}}}},"/admin/outbox/stats":{"get":{"tags":["Admin"],"summary":"Outbox queue health","description":"Return pending outbox count and oldest pending event timestamp.","operationId":"outbox_stats_admin_outbox_stats_get","security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Outbox Stats Admin Outbox Stats Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/coupons/import":{"post":{"tags":["Admin"],"summary":"Import legacy WooCommerce coupons (read-only origin)","description":"Bulk-import existing WooCommerce coupons as `legacy_origin=True`.\nThese are read-only in the MS — no outbox events are emitted.\nExisting codes (by `code`) are skipped (idempotent).","operationId":"import_legacy_coupons_admin_coupons_import_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegacyImportRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Import Legacy Coupons Admin Coupons Import Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/coupons/{code}/restore":{"post":{"tags":["Admin"],"summary":"Restore a soft-deleted coupon","description":"Un-delete a coupon by clearing deleted_at. Does not re-create it in WooCommerce\n(caller must trigger a sync if WC replica is also needed).","operationId":"restore_coupon_admin_coupons__code__restore_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Restore Coupon Admin Coupons  Code  Restore Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/coupons/expire":{"post":{"tags":["Admin"],"summary":"Trigger expiration sweep manually","description":"Soft-delete all non-deleted coupons expired hace más de\nEXPIRED_RETENTION_DAYS días (misma ventana de gracia que el sweeper).\nThe background sweeper does this automatically, but this endpoint\nallows manual triggering (e.g., after timezone changes or test scenarios).","operationId":"trigger_expiration_sweep_admin_coupons_expire_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Trigger Expiration Sweep Admin Coupons Expire Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/cache/stats":{"get":{"tags":["Admin"],"summary":"Cache hit/miss counters","description":"Redis cache hit/miss counters accumulated since last process start.","operationId":"cache_stats_admin_cache_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Cache Stats Admin Cache Stats Get"}}}}},"security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}]}},"/admin/headless/cache/refresh":{"post":{"tags":["Admin"],"summary":"Refrescar cache de productos/categorías headless","description":"Invalida y recarga la cache Redis para los IDs dados.\n\nSi ambos campos están vacíos, refresca **todos** los IDs vistos (productos y categorías).\n\nLlamar desde WooCommerce/headless tras editar un producto o categoría para evitar esperar el ciclo natural del warmer (55 min).","operationId":"headless_cache_refresh_admin_headless_cache_refresh_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeadlessCacheRefreshRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Headless Cache Refresh Admin Headless Cache Refresh Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/coupons/migrate-from-wc":{"post":{"tags":["Admin"],"summary":"Bulk-import coupons from WooCommerce","description":"Paginate WooCommerce coupons and import them locally as legacy_origin=True.\nExisting codes are skipped (idempotent). Respects LEGACY_AUTO_IMPORT_ENABLED.","operationId":"migrate_from_wc_admin_coupons_migrate_from_wc_post","security":[{"BearerAuth":[]}],"parameters":[{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateFromWcRequest","default":{"max_pages":10,"per_page":100}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Migrate From Wc Admin Coupons Migrate From Wc Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/coupons/wc-lookup/{code}":{"get":{"tags":["Admin"],"summary":"Lookup a coupon in WooCommerce without persisting","description":"Fetch raw WooCommerce coupon data for inspection. Does not import anything locally.","operationId":"wc_lookup_admin_coupons_wc_lookup__code__get","security":[{"BearerAuth":[]}],"parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Country","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Country"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Wc Lookup Admin Coupons Wc Lookup  Code  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/dev/sign-webhook":{"post":{"tags":["Dev"],"summary":"Calcular firma HMAC para un body (solo dev)","description":"Devuelve `signature` + `timestamp` para usar en `X-Adipa-Signature` / `X-Adipa-Timestamp` al llamar a `/api/v1/webhooks/coupons/{code}/apply` manualmente.\n\n**Atención:** los bytes del body al llamar al webhook real deben ser **idénticos** a los firmados aquí — el menor cambio (whitespace, orden de keys, encoding) rompe la firma. Para evitar este problema, usa preferentemente `POST /api/v1/dev/webhook-apply/{code}` que firma y reenvía en una sola llamada.","operationId":"sign_webhook_api_v1_dev_sign_webhook_post","responses":{"200":{"description":"Firma calculada.","content":{"application/json":{"schema":{},"example":{"signature":"13f2861837ba0363f3be3a177b3ccd4e3c127af12a573f9a23a7b9fcdabe46c8","timestamp":"1779900410","body":"{\"order_id\":\"ORD-001\",\"external_reference\":\"pi_3PZabc\"}"}}}},"401":{"description":"X-API-Key inválida."},"404":{"description":"Endpoint deshabilitado (ENV=prod)."}},"parameters":[{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}]}},"/api/v1/dev/webhook-apply/{code}":{"post":{"tags":["Dev"],"summary":"Firmar y reenviar al webhook real (solo dev)","description":"Endpoint de conveniencia para testing local. Recibe el payload, lo firma con el `hmac_secret` del país, y hace un POST interno a `/api/v1/webhooks/coupons/{code}/apply` con los mismos bytes raw + headers HMAC.\n\nGarantiza que los bytes firmados sean **byte-idénticos** a los validados (los reenvía como `httpx.content=raw`), eliminando problemas de serialización JSON entre cliente y servidor.\n\nDevuelve la respuesta real del webhook (status 200/201/401/404/422) sin modificarla.\n\n### Uso desde Bruno\n\nURL: `{{base_url}}/api/v1/dev/webhook-apply/{{coupon_code}}`\nHeaders: `X-API-Key`, `X-Country`, `X-Webhook-Source` (opcional)\nBody: igual que el endpoint real `/api/v1/webhooks/coupons/{code}/apply`.\n\n### Diferencia con `sign-webhook`\n\n| Endpoint | Comportamiento |\n|---|---|\n| `sign-webhook` | Solo devuelve firma. Tú haces la 2da llamada. Frágil. |\n| `webhook-apply/{code}` | Firma + llama internamente. Sola call desde el cliente. Robusto. |","operationId":"webhook_apply_signed_api_v1_dev_webhook_apply__code__post","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"X-Language","in":"header","required":false,"schema":{"type":"string","enum":["es","en"],"default":"es"},"description":"Idioma de respuesta para mensajes de error/validación. Prioridad: `X-Language` sobre `Accept-Language`."}],"responses":{"200":{"description":"Replay idempotente (passthrough).","content":{"application/json":{"schema":{}}}},"201":{"description":"Webhook procesó el apply (passthrough de la respuesta real)."},"401":{"description":"X-API-Key inválida."},"404":{"description":"Endpoint deshabilitado (ENV=prod) o cupón no encontrado."},"422":{"description":"Body inválido o validación fallida (passthrough)."}}}}},"components":{"schemas":{"ApiResponse":{"properties":{"data":{"title":"Data"},"meta":{"anyOf":[{"$ref":"#/components/schemas/MetaResponse"},{"type":"null"}]},"error":{"anyOf":[{},{"type":"null"}],"title":"Error"}},"type":"object","required":["data"],"title":"ApiResponse"},"ApplyCouponRequest":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","examples":["jaime@adipa.cl"]},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id","description":"Order ID from the caller system.","examples":["ORD-001"]},"product_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Product Ids","examples":[[2843]]},"cart_total":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cart Total","examples":[49990.0]},"already_applied":{"type":"boolean","title":"Already Applied","description":"True SOLO cuando el llamador es un hook interno de WooCommerce (woocommerce_apply_coupon) sobre un cupón ya aplicado, nunca texto tecleado por el usuario. Ver ValidateCouponRequest.","default":false}},"type":"object","title":"ApplyCouponRequest"},"BulkCouponCreate":{"properties":{"quantity":{"type":"integer","title":"Quantity","description":"Cuántos cupones generar (1-100).","examples":[10]},"discount_type":{"type":"string","enum":["fixed_amount","percent"],"title":"Discount Type","examples":["percent"]},"amount":{"type":"number","title":"Amount","examples":[5000.0]},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type","examples":["campaign"]},"usage_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Usage Limit","default":1,"examples":[1]},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","examples":["2026-12-31T23:59:59Z"]},"expiration_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expiration Days","examples":[365]},"categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Categories","examples":[[12]]},"products":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Products","examples":[[2843]]},"excluded_products":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Excluded Products","examples":[[999]]},"minimum_expense":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Minimum Expense","examples":[10000.0]},"maximum_expense":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Maximum Expense","examples":[500000.0]},"excluded_categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Excluded Categories","examples":[[15]]},"prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prefix","examples":["GC"]}},"type":"object","required":["quantity","discount_type","amount"],"title":"BulkCouponCreate"},"BulkExtendRequest":{"properties":{"codes":{"items":{"type":"string"},"type":"array","maxItems":200,"minItems":1,"title":"Codes","description":"Códigos de cupón a extender (máx. 200 por llamada).","examples":[["CPN-ABC123","CPN-XYZ789"]]},"extend_days":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Extend Days","description":"Extiende la expiración N días desde AHORA (hora del servidor), igual que `CouponUpdate.extend_days`. Mutuamente excluyente con `expires_at` — debe enviarse exactamente uno de los dos.","examples":[30]},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"Fija la expiración absoluta de CADA código del batch a esta fecha exacta, reemplazando la que tuviera. Puede mover la expiración hacia adelante o hacia atrás (acortar la vigencia) — no hay validación de que sea posterior a la actual. Mutuamente excluyente con `extend_days` — debe enviarse exactamente uno de los dos.","examples":["2026-12-31T23:59:59Z"]},"change_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Reason","description":"Motivo del cambio, capturado en el historial de versiones de cada cupón.","examples":["Reactivación masiva temporada 2026"]}},"type":"object","required":["codes"],"title":"BulkExtendRequest"},"CampaignUpsertRequest":{"properties":{"email":{"type":"string","title":"Email","examples":["santiago@adipa.co"]},"cart_total":{"type":"number","title":"Cart Total","examples":[45000.0]},"product_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Product Ids","examples":[[30000086,33]]}},"type":"object","required":["email","cart_total"],"title":"CampaignUpsertRequest"},"CouponCreate":{"properties":{"discount_type":{"type":"string","enum":["fixed_amount","percent"],"title":"Discount Type","description":"Tipo de descuento. `percent` → % del carrito; `fixed_amount` → monto fijo. fixed_amount se mapea a WooCommerce como `fixed_product` cuando hay productos y `fixed_cart` cuando aplica al carrito completo.","examples":["percent"]},"amount":{"type":"number","title":"Amount","description":"Monto del descuento (% si percent, valor absoluto si fixed_amount).","examples":[15.0]},"type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Type","description":"Etiqueta de tipo de cupón (referencia al catálogo `coupon_types` del país). Opcional, solo para segmentación/métricas — NO aplica reglas de negocio. Si se envía, debe existir y estar habilitado en el catálogo.","examples":["campaign"]},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Restricción por email (opcional).","examples":["jaime@adipa.cl"]},"usage_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Usage Limit","description":"Máximo de aplicaciones del cupón. null = ilimitado.","examples":[1]},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","description":"Expiración absoluta (opcional).","examples":["2026-12-31T23:59:59Z"]},"expiration_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expiration Days","description":"Alternativa a expires_at — fija expires_at = now + N días.","examples":[30]},"categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Categories","description":"IDs de categorías WooCommerce permitidas.","examples":[[12,34]]},"products":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Products","description":"IDs de productos WooCommerce permitidos.","examples":[[2843]]},"excluded_products":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Excluded Products","description":"IDs de productos WooCommerce excluidos del descuento.","examples":[[999]]},"minimum_expense":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Minimum Expense","description":"Gasto mínimo del carrito para que aplique el cupón (WC minimum_amount).","examples":[10000.0]},"maximum_expense":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Maximum Expense","description":"Gasto máximo del carrito para que aplique el cupón (tope de carrito).","examples":[500000.0]},"excluded_categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Excluded Categories","description":"IDs de categorías WooCommerce excluidas del descuento.","examples":[[15,18]]},"new_customer_only":{"type":"boolean","title":"New Customer Only","description":"Restringe el cupón a quien nunca compró en la tienda del país. Se verifica contra adipa-headless en cada validación, contando órdenes `completed`, `processing` y `on-hold` (canceladas y reembolsadas NO cuentan). Fail-closed: sin email, o si headless no responde, el cupón se rechaza.","default":false,"examples":[false]},"prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prefix","description":"Prefijo del código generado. Default `CPN`. Ignorado si se envía `code`.","examples":["PROMO"]},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code","description":"Código exacto del cupón (en vez de generar uno con prefijo + sufijo aleatorio). Solo letras, números, guiones y guiones bajos, 3-40 caracteres. Falla si ya existe.","examples":["BLACK-CURSOS"]}},"type":"object","required":["discount_type","amount"],"title":"CouponCreate"},"CouponTypeCreate":{"properties":{"slug":{"type":"string","title":"Slug","description":"Identificador único del tipo (kebab/snake). Inmutable tras crear.","examples":["flash_sale"]},"name":{"type":"string","title":"Name","description":"Nombre legible para métricas/UI.","examples":["Flash Sale"]},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","examples":["Promoción relámpago"]},"enabled":{"type":"boolean","title":"Enabled","default":true,"examples":[true]}},"type":"object","required":["slug","name"],"title":"CouponTypeCreate"},"CouponTypeUpdate":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name","examples":["Flash Sale"]},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","examples":["Promoción relámpago"]},"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled","examples":[true]}},"type":"object","title":"CouponTypeUpdate"},"CouponUpdate":{"properties":{"amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Amount","examples":[20.0]},"discount_type":{"anyOf":[{"type":"string","enum":["fixed_amount","percent"]},{"type":"null"}],"title":"Discount Type","description":"Cambia el tipo de descuento (`percent` | `fixed_amount`). Se remapea a WooCommerce igual que en creación.","examples":["percent"]},"usage_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Usage Limit","examples":[5]},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","examples":["2026-12-31T23:59:59Z"]},"extend_days":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Extend Days","description":"Extiende la expiración N días desde AHORA (hora del servidor), en vez de fijar una fecha absoluta. Útil para reactivación 'por activaciones' de comercial/embajadores. Si se envía junto con `expires_at`, `extend_days` tiene precedencia.","examples":[30]},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","examples":["jaime@adipa.cl"]},"maximum_expense":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Maximum Expense","examples":[500000.0]},"excluded_categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Excluded Categories","examples":[[15]]},"allowed_product_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Allowed Product Ids","description":"Reemplaza los IDs de producto elegibles del cupón.","examples":[[2843]]},"individual_use":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Individual Use","examples":[true]},"change_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Change Reason","description":"Motivo del cambio, capturado en el historial de versiones (`CouponVersion.change_reason`).","examples":["Reactivación temporada 2026"]}},"type":"object","title":"CouponUpdate"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HeadlessCacheRefreshRequest":{"properties":{"product_ids":{"items":{"type":"integer"},"type":"array","title":"Product Ids","description":"IDs de productos a refrescar. Vacío = todos los vistos.","examples":[[101,202,303]]},"category_ids":{"items":{"type":"integer"},"type":"array","title":"Category Ids","description":"IDs de categorías a refrescar. Vacío = todas las vistas.","examples":[[10,20]]}},"type":"object","title":"HeadlessCacheRefreshRequest"},"LegacyCouponItem":{"properties":{"code":{"type":"string","maxLength":100,"minLength":1,"title":"Code"},"discount_type":{"type":"string","title":"Discount Type"},"amount":{"type":"number","title":"Amount"},"usage_limit":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Usage Limit"},"email_restriction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email Restriction"},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At"},"wc_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wc Id"}},"type":"object","required":["code","discount_type","amount"],"title":"LegacyCouponItem"},"LegacyImportRequest":{"properties":{"coupons":{"items":{"$ref":"#/components/schemas/LegacyCouponItem"},"type":"array","maxItems":500,"minItems":1,"title":"Coupons"}},"type":"object","required":["coupons"],"title":"LegacyImportRequest"},"MetaResponse":{"properties":{"page":{"type":"integer","title":"Page","examples":[1]},"per_page":{"type":"integer","title":"Per Page","examples":[20]},"total":{"type":"integer","title":"Total","examples":[42]}},"type":"object","required":["page","per_page","total"],"title":"MetaResponse"},"MigrateFromWcRequest":{"properties":{"max_pages":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Max Pages","default":10},"per_page":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Per Page","default":100}},"type":"object","title":"MigrateFromWcRequest"},"ReferralRewardRequest":{"properties":{"email":{"type":"string","title":"Email","examples":["referidor@adipa.cl"]},"tier_slug":{"type":"string","title":"Tier Slug","examples":["nivel_2"]},"idempotency_key":{"type":"string","maxLength":80,"minLength":1,"title":"Idempotency Key","examples":["3f9a2c1e0b7d4a5f6e8c9d0b1a2f3e4d5c6b7a8f"]},"discount_type":{"type":"string","enum":["fixed_amount","percent"],"title":"Discount Type","examples":["percent"]},"amount":{"type":"number","title":"Amount","examples":[20.0]},"expiration_days":{"type":"integer","exclusiveMinimum":0.0,"title":"Expiration Days","examples":[90]},"categories":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Categories","examples":[[123]]},"products":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Products","examples":[[456]]},"minimum_expense":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Minimum Expense","examples":[null]}},"type":"object","required":["email","tier_slug","idempotency_key","discount_type","amount","expiration_days"],"title":"ReferralRewardRequest","description":"Payload from adipa-headless' `EmitReferralRewardsJob` — one call per\n(referrer, tier) unlocked. `idempotency_key` is headless'\n`sha1(\"{country}:{user_id}:{tier_slug}\")`, computed once and retried\nverbatim on failure, so this is the sole dedup key: two calls with the\nsame key must return the same coupon, never create a second one."},"ReferralRewardResponse":{"properties":{"code":{"type":"string","title":"Code","examples":["REF-8KZQ2P"]},"expires_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Expires At","examples":["2026-10-28T00:00:00Z"]},"created":{"type":"boolean","title":"Created","examples":[true]}},"type":"object","required":["code","created"],"title":"ReferralRewardResponse","description":"Flat (unenveloped) body — `ReferralRewardCouponsClient` in headless\nreads `code`/`expires_at`/`created` off the top level, not under `data`."},"ValidateCouponRequest":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","examples":["jaime@adipa.cl"]},"product_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Product Ids","examples":[[2843]]},"cart_total":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cart Total","examples":[49990.0]},"already_applied":{"type":"boolean","title":"Already Applied","description":"True SOLO cuando el llamador es un hook interno de WooCommerce revalidando un cupón que ya quedó aplicado en el carrito (nunca cuando el código viene de texto tecleado por el usuario). Afecta únicamente a los 6 cupones compartidos de venta nocturna: sin este flag, escribir uno de esos códigos directamente se rechaza siempre, sin importar si el email es participante de la campaña.","default":false}},"type":"object","title":"ValidateCouponRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookApplyRequest":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email","description":"Customer email. Validated against coupon's email_restriction.","examples":["jaime@adipa.cl"]},"order_id":{"type":"string","title":"Order Id","description":"Order ID in the caller's system. Used together with `external_reference` for idempotency.","examples":["ORD-WH-001"]},"external_reference":{"type":"string","title":"External Reference","description":"Gateway-specific transaction reference (Stripe payment_intent, MercadoPago payment_id, etc.). Used together with `order_id` for idempotency.","examples":["pi_3PZabcStripeRef001"]},"cart_total":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cart Total","description":"Order subtotal before discount, in country currency.","examples":[49990.0]},"product_ids":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Product Ids","description":"WooCommerce product IDs in the cart. Validated against coupon's allowed_product_ids.","examples":[[2843]]},"amount_applied":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Amount Applied","description":"Discount amount actually applied to this order.","examples":[9998.0]}},"type":"object","required":["order_id","external_reference"],"title":"WebhookApplyRequest","description":"Payload sent by a payment gateway after a transaction completes.","examples":[{"amount_applied":9998.0,"cart_total":49990,"email":"jaime@adipa.cl","external_reference":"pi_3PZabcStripeRef001","order_id":"ORD-WH-001","product_ids":[2843]}]}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key — valor de `API_KEY` en `.env` (dev default: `secret-api-key-change-me`)"},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Keycloak JWT — obtener con `POST /realms/adipa/protocol/openid-connect/token`"}}}}