# API Pública — Canasta Básica

Expone precios de productos de la canasta básica de México para ser consumida por la app React embebida en WordPress mediante ReactPress.

## Configuración

### CORS

Configurado en `config/cors.php`. Orígenes permitidos:

- `https://pesosycentavos.com`
- `https://www.pesosycentavos.com`
- `http://localhost:5173` (desarrollo local con Vite)

Para agregar staging u otro origen, añadir la URL completa al arreglo `allowed_origins`.

### Sin autenticación

Los endpoints son públicos (solo GET). No requieren token ni CSRF. Se protegen con **rate limiting** estándar de Laravel (`throttle:api` = 60 req/min).

### Consumo desde React (fetch/Axios)

```js
// Sin credenciales ni CSRF — la API es pública
const BASE_URL = 'http://localhost:8097'; // Cambiar en producción a https://api.pesosycentavos.com

const response = await fetch(`${BASE_URL}/api/basic-basket/products`, {
  headers: { 'Accept': 'application/json' },
});
const data = await response.json();

// Las URLs de imágenes en la respuesta son absolutas:
console.log(data.data[0].image_url); // "http://localhost:8097/images/productos/23_tortilla-de-maiz.jpg"
console.log(data.data[0].lowest_price_store_logo_url); // "http://localhost:8097/images/tiendas/bodega-aurrera.png"
```

---

## Endpoints

### 1. Opciones de filtros

```
GET /api/basic-basket/filters
```

Devuelve todos los productos activos y tiendas activas para poblar los selects del frontend. No requiere paginación.

#### Ejemplo de respuesta

```json
{
  "data": {
    "products": [
      { "slug": "arroz-en-grano", "name": "Arroz en grano" },
      { "slug": "tortilla-maiz-kilo", "name": "Tortilla de maíz por kilo" }
    ],
    "stores": [
      { "slug": "bodega-aurrera", "name": "Bodega Aurrera", "logo_url": "http://localhost:8097/images/tiendas/bodega-aurrera.png" },
      { "slug": "chedraui",       "name": "Chedraui",       "logo_url": "http://localhost:8097/images/tiendas/chedraui.png" },
      { "slug": "soriana",        "name": "Soriana",        "logo_url": "http://localhost:8097/images/tiendas/soriana.jpg" }
    ]
  }
}
```

Notas:
- Los productos están ordenados por nombre.
- Las tiendas están ordenadas por nombre.
- `logo_url` es una URL absoluta o `null` si la tienda no tiene logo configurado.
- No incluye productos ni tiendas inactivos.

---

### 2. Listado paginado de productos

```
GET /api/basic-basket/products
```

#### Parámetros de query

| Parámetro  | Tipo    | Default | Descripción                                                                 |
|------------|---------|---------|-----------------------------------------------------------------------------|
| `page`     | integer | 1       | Número de página.                                                           |
| `per_page` | integer | 12      | Resultados por página. Máximo: 48.                                          |
| `search`   | string  | —       | Busca por nombre, slug o categoría del producto.                            |
| `product`  | string  | —       | Filtra por slug exacto de producto (ej. `tortilla-maiz-kilo`).              |
| `category` | string  | —       | Filtra por categoría exacta (retrocompatibilidad; preferir `product`).      |
| `store`    | string  | —       | Filtra por slug de tienda (ej. `bodega-aurrera`, `soriana`).                |

> **Nota:** El parámetro `product` es el preferido para filtrar por producto específico desde el frontend.
> El select "Productos" debe usar `?product={slug}`. El parámetro `category` se mantiene por retrocompatibilidad.

#### Ejemplo de respuesta

```json
{
  "data": [
    {
      "id": 1,
      "slug": "tortilla-maiz-kilo",
      "name": "Tortilla de maíz por kilo",
      "category": "Tortillas",
      "expected_unit": "kg",
      "image_url": "http://localhost:8097/images/productos/23_tortilla-de-maiz.jpg",
      "lowest_price": "24.90",
      "currency": "MXN",
      "lowest_price_store": "Bodega Aurrera",
      "lowest_price_store_slug": "bodega-aurrera",
      "lowest_price_store_logo_url": "http://localhost:8097/images/tiendas/bodega-aurrera.png",
      "lowest_price_checked_at": "2026-05-26T02:15:00.000000Z",
      "is_price_stale": false,
      "latest_checked_at": "2026-05-26T02:15:00.000000Z",
      "available_stores_count": 3
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": null },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 2,
    "per_page": 12,
    "to": 12,
    "total": 24
  }
}
```

---

### 2. Detalle de producto

```
GET /api/basic-basket/products/{slug}
```

#### Ejemplo de respuesta

```json
{
  "data": {
    "id": 1,
    "slug": "tortilla-maiz-kilo",
    "name": "Tortilla de maíz por kilo",
    "category": "Tortillas",
    "expected_unit": "kg",
    "image_url": "http://localhost:8097/images/productos/23_tortilla-de-maiz.jpg",
    "latest_checked_at": "2026-05-26T02:15:00.000000Z",
    "stores": [
      {
        "store_id": 1,
        "store_slug": "bodega-aurrera",
        "store_name": "Bodega Aurrera",
        "store_logo_url": "http://localhost:8097/images/tiendas/bodega-aurrera.png",
        "price": "24.90",
        "currency": "MXN",
        "checked_at": "2026-05-26T02:15:00.000000Z",
        "is_price_stale": false,
        "observation_id": 42,
        "product_title_detected": "Tortilla de maíz por kilo",
        "seller_detected": "Bodega Aurrera Despensa a tu Casa",
        "external_url": "https://despensa.bodegaaurrera.com.mx/producto/tortilla-1kg",
        "external_url_type": "store_product"
      },
      {
        "store_id": 2,
        "store_slug": "soriana",
        "store_name": "Soriana",
        "store_logo_url": "http://localhost:8097/images/tiendas/soriana.jpg",
        "price": "26.50",
        "currency": "MXN",
        "checked_at": "2026-05-26T02:15:00.000000Z",
        "is_price_stale": false,
        "observation_id": 43,
        "product_title_detected": "Tortilla De Maíz Blanca 1 Kg",
        "seller_detected": "Soriana.com",
        "external_url": null,
        "external_url_type": null
      }
    ]
  }
}
```

#### Respuesta 404

```json
{ "message": "Producto no encontrado." }
```

---

## Lógica de precios y fallback

1. Para cada producto activo, se obtienen sus **targets activos** por tienda.
2. Por cada target, se busca la **observación válida más reciente** (`latestOfMany('observed_at')` de Eloquent).
3. Si la observación más reciente **no es de hoy**, se marca `is_price_stale = true`. Sigue mostrándose como precio de referencia.
4. Si no existe ninguna observación para un target, `price = null`.
5. El `lowest_price` del listado es el mínimo entre las observaciones disponibles (hoy o históricas).
6. **Los `price_observation_failures` no se usan para calcular precios.**
7. No se consulta DataForSEO en tiempo real.

---

## Campo `external_url` en el detalle de tienda

| Caso | `external_url` | `external_url_type` |
|------|---------------|---------------------|
| `store_product_url` tiene URL | URL del producto en tienda | `"store_product"` |
| `store_product_url` es null | `null` | `null` |

El frontend debe **ocultar o deshabilitar** el botón "Ir al producto" cuando `external_url = null`.

---

## Diferencia entre campos de URL

| Campo | Dónde vive | Qué representa | Se usa para |
|-------|-----------|----------------|-------------|
| `store_product_url` | `store_product_targets` | URL real del producto en la tienda, configurada manualmente | Botón "Ir al producto" en el frontend |
| `expected_url` | `store_product_targets` | URL para señal de matching (+40 pts) si el resultado de DataForSEO incluye directUrl | Scoring del sincronizador. **No es para frontend.** |
| `shopping_url` | `price_observations` | URL proxy de Google Shopping (`google.com.mx/search?ibp=oshop...`) | Auditoría técnica. **No es la URL real del producto.** |

**Nota sobre URLs de imágenes:** Tanto `image_url` (de productos) como `store_logo_url` (de tiendas) y `lowest_price_store_logo_url` (en listado) se devuelven como **URLs absolutas** en la API (ej: `http://localhost:8097/images/productos/23_tortilla-de-maiz.jpg`). Se construyen automáticamente con el helper `url()` en los Resources.

---

## Estructura de archivos de imágenes

Las imágenes se encuentran en `public/images/` y se sirven directamente al navegador sin procesar:

```
public/
├── images/
│   ├── productos/        ← Imágenes de productos canasta básica
│   │   ├── 01_aceite-vegetal-comestible.jpg
│   │   ├── 02_arroz-en-grano.jpg
│   │   ├── 03_atun-en-hojuela.jpg
│   │   ├── ...
│   │   └── 23_tortilla-de-maiz.jpg
│   └── tiendas/          ← Logos de tiendas
│       ├── bodega-aurrera.png
│       ├── soriana.jpg
│       └── chedraui.png
```

**Cómo servir las imágenes:**
- No require configuración especial. Las imágenes en `public/` se sirven automáticamente.
- Se acceden via URLs relativas (ej: `/images/productos/23_tortilla-de-maiz.jpg`).
- La API convierte estas URLs relativas a **URLs absolutas** usando el helper `url()`.

---

## Cómo actualizar imágenes y URLs desde seeders

### Agregar imagen a un producto

Editar `database/seeders/BasicBasketProductSeeder.php`:

```php
['slug' => 'tortilla-maiz-kilo', 'name' => 'Tortilla de maíz por kilo', ..., 'image_url' => '/images/productos/23_tortilla-de-maiz.jpg', ...],
```

1. Colocar el archivo en `public/images/productos/` (requiere archivo existente en la carpeta).
2. Correr `php artisan db:seed --class=BasicBasketProductSeeder`.

**Nota:** Las imágenes de productos se encuentran en `public/images/productos/` listadas como:
- `01_aceite-vegetal-comestible.jpg`
- `02_arroz-en-grano.jpg`
- `03_atun-en-hojuela.jpg`
- ... (y más)
- `23_tortilla-de-maiz.jpg`

### Agregar logo a una tienda

Editar `database/seeders/StoreSeeder.php`:

```php
'logo_url' => '/images/tiendas/bodega-aurrera.png',
```

1. Colocar el archivo en `public/images/tiendas/`.
2. Correr `php artisan db:seed --class=StoreSeeder`.

**Logos disponibles:**
- `bodega-aurrera.png`
- `soriana.jpg`
- `chedraui.png`

### Configurar la URL real del producto por tienda

Editar `targetOverrides()` en `database/seeders/StoreProductTargetSeeder.php`:

```php
'tortilla-maiz-kilo@bodega-aurrera' => [
    // ... señales de matching existentes ...
    'store_product_url' => 'https://despensa.bodegaaurrera.com.mx/producto/tortilla-1kg',
],
'tortilla-maiz-kilo@soriana' => [
    // ... señales de matching existentes ...
    'store_product_url' => 'https://www.soriana.com/alimentos/tortillas/tortilla-de-maiz-blanca-1-kg/p/00750958302271',
],
```

Luego correr:
```sh
php artisan db:seed --class=StoreProductTargetSeeder
```

**Notas:**
- Si la URL del producto no está disponible o es inestable, dejar `store_product_url => null`. El frontend ocultará el botón.
- No uses `shopping_url` (URL de Google Shopping) como valor de `store_product_url`.
- `expected_url` se usa para matching interno del sincronizador; no lo confundas con `store_product_url`.
- Las imágenes de productos y tiendas se sirven como URLs absolutas desde la API (ej: `http://localhost:8097/images/productos/23_tortilla-de-maiz.jpg`), construidas con el helper `url()`.

---

## Archivos principales

| Archivo | Rol |
|---------|-----|
| `app/Services/BasicBasketPriceService.php` | Queries de productos con precios precargados; soporta filtros `search`, `product`, `category`, `store` |
| `app/Http/Controllers/BasicBasketProductController.php` | Endpoints `index`, `show` y `filters` |
| `app/Http/Resources/BasicBasketProductListResource.php` | Resource de listado |
| `app/Http/Resources/BasicBasketProductDetailResource.php` | Resource de detalle |
| `app/Http/Resources/StorePriceResource.php` | Resource de tienda con precio y external_url |
| `config/cors.php` | Orígenes CORS permitidos (incluye localhost:5173) |
| `routes/api.php` | Registro de rutas |
| `database/seeders/BasicBasketProductSeeder.php` | Productos + image_url |
| `database/seeders/StoreSeeder.php` | Tiendas + logo_url |
| `database/seeders/StoreProductTargetSeeder.php` | Targets + store_product_url |



