<!-- https://pricto.dev/docs/modelo-de-datos · versión en inglés: https://pricto.dev/docs/modelo-de-datos.en.md -->

# Modelo de datos

Tres objetos explican casi todo:

- **Producto**: el artículo, igual en todas las tiendas (`prd_`). Tiene marca, categoría, tamaño y códigos de barras (GTIN).
- **Oferta**: ese producto en una tienda (`ofr_`), con su URL y su título original.
- **Observación**: lo que vimos en una oferta en un momento: precio, condiciones, stock y de dónde salió. Las observaciones no se modifican; el historial es la serie de observaciones.

## Precio: qué significa cada campo

`price` es lo que paga cualquier persona sin condiciones: el precio de oferta si existe, si no el de lista. Lo condicionado va aparte, para que nadie lo presente como "el precio".

| Campo | Qué es |
|---|---|
| `list_price` | Precio normal publicado |
| `sale_price` | Precio rebajado sin condiciones, o `null` |
| `price` | El menor de los dos: lo que paga cualquiera |
| `conditional_prices[]` | Precios que exigen algo distinto de la cantidad: medio de pago, nivel de club, app o cupón |
| `card_price`, `card_label` | Atajo: el menor precio con tarjeta o membresía (por ejemplo "Tarjeta Cencosud") |
| `promotions[]` | Exigen cantidad o combinación: "2 x $2.590", la segunda unidad, porcentaje |
| `unit_price` | Precio por unidad base (kg, l, unidad, m o m2), para comparar tamaños |
| `installments` | Cuotas sin interés de la oferta |
| `availability` | Stock: `in_stock`, `out_of_stock`, `limited` o `unknown` |

## Procedencia

Cada precio dice de dónde salió, para que cualquiera lo pueda comprobar:

| Campo | Qué es |
|---|---|
| `url` | La ficha del producto en la tienda |
| `observed_at` | Fecha y hora de la captura, en UTC |
| `observed_local_date` | Fecha de la captura en la zona de la tienda |
| `capture_method` | `public_page` (la página pública), `public_api` (un endpoint abierto del sitio), `retailer_feed` (entregado por la tienda) u `open_data` (un dataset público) |
| `meta.attribution.text` | Una línea lista para citar: marca, tienda, hora e id |

## Mismo producto entre tiendas

Cada oferta trae `match`: cómo se unió al producto y con qué confianza.

| Campo | Qué es |
|---|---|
| `match.method` | Cómo se unió: por GTIN, por reglas o por un modelo |
| `match.confidence` | Entre 0 y 1 |
| `match.kind` | `identical` (el mismo artículo), `pack` (N unidades del producto, ver `pack_ratio`) o `equivalent` (sustituto de otra marca) |
| `match.pack_ratio` | Cuántas unidades trae el pack |

Por defecto los precios y las comparaciones agrupan solo ofertas `identical` con confianza de 0,85 o más. `min_confidence` y `match_kinds` (por ejemplo `identical,pack`) amplían el grupo, y `meta.warnings` avisa cuando quedaron ofertas fuera. En un pack, compara por `unit_price`.

## Calidad

Los precios se entregan tal como los publica la tienda, nunca corregidos. Si una validación ve algo raro, el precio llega con `quality_status: "warning"` y la razón en `quality_flags[]`: `discount_over_85` (descuento sobre 85 % del precio normal) o `round_price_low` (precio redondo de $1.000 o menos). Si una observación se enmendó después de capturada, `corrected` es `true` y el original se guarda.

## Moneda, impuestos e idioma

- Montos en la moneda de la tienda; CLP no tiene decimales. `currency=USD` agrega `converted` con la tasa, su fecha y su fuente.
- `tax_included` dice si el precio publicado incluye impuestos (en Chile, sí). `tax_rate` y `price_ex_tax` se derivan de la regla del país (`tax_rule_id`), o vienen en `null` si no hay regla.
- Nombres y categorías en español o inglés según `lang`; `names` trae todos los idiomas y `title_original`, el título de la tienda sin traducir.

## Taxonomía

Las categorías son un árbol con ids legibles (`food.dairy.milk`): filtrar por `food.dairy` incluye sus subcategorías. El árbol completo está en [`GET /v1/categories`](/docs/api/list-categories).

## Campos

### Precio en una tienda (`RetailerPrice`)

| Campo | Tipo | Descripción |
|---|---|---|
| `currency` **requerido** | `string` |  |
| `price` **requerido** | `number` |  |
| `list_price` **requerido** | `number` |  |
| `sale_price` | `number \| null` |  |
| `card_price` | `number \| null` | Convenience: the lowest price in conditional_prices that requires a payment method or membership. |
| `card_label` | `string \| null` |  |
| `conditional_prices` | `ConditionalPrice[]` | Every price that requires a condition other than quantity (payment method, loyalty tier, app, coupon), as published. |
| `conditional_prices[].kind` **requerido** | `string` | Valores / values: `payment_method`, `membership`, `app_only`, `coupon`, `other`. |
| `conditional_prices[].label` **requerido** | `string` | As shown by the retailer. |
| `conditional_prices[].price` **requerido** | `number` |  |
| `conditional_prices[].payment_method` | `string \| null` |  |
| `conditional_prices[].membership_tier` | `string \| null` |  |
| `conditional_prices[].installments_count` | `integer \| null` |  |
| `conditional_prices[].interest_free` | `boolean \| null` |  |
| `unit_price` | `object \| null` |  |
| `unit_price.amount` **requerido** | `number` |  |
| `unit_price.per` **requerido** | `string` | Valores / values: `kg`, `l`, `unit`, `m`, `m2`. |
| `promotions` | `Promotion[]` |  |
| `promotions[].type` **requerido** | `string` | Valores / values: `multi_buy`, `nth_unit`, `percent_off`, `amount_off`, `bundle`, `gift`, `other`. |
| `promotions[].label` **requerido** | `string` | As shown by the retailer, e.g. "2 x $2.990". |
| `promotions[].min_quantity` | `integer \| null` |  |
| `promotions[].effective_unit_price` | `number \| null` | Price per item when the promotion applies. |
| `promotions[].requires_card` | `string \| null` | Payment method required, if any. |
| `promotions[].valid_until` | `string (date) \| null` |  |
| `availability` **requerido** | `string` | Valores / values: `in_stock`, `out_of_stock`, `limited`, `unknown`. |
| `tax_included` | `boolean` | As shown by the retailer (observed). |
| `tax_rate` | `number \| null` | Derived. Main VAT rate for this country and category, null when not known. |
| `price_ex_tax` | `number \| null` | Derived from price and tax_rate. Null when tax_rate is null. |
| `tax_rule_id` | `string \| null` | Versioned rule used to derive tax_rate. |
| `other_taxes_included` | `boolean \| null` | True when other taxes remain in price_ex_tax (e.g. Chile IABA on sweetened drinks). |
| `installments` | `object` |  |
| `observed_at` **requerido** | `string (date-time)` |  |
| `observed_local_date` | `string (date)` | Capture date in the retailer time zone. Daily series use this date. |
| `capture_method` **requerido** | `string` | How the price was obtained, part of the provenance with `url` and `observed_at`. `public_page`: read from the retailer's public web page (HTML, JSON-LD or the page payload). `public_api`: an open, unauthenticated JSON endpoint of the retailer's own site. `retailer_feed`: delivered by the retailer under an agreement. `open_data`: a public dataset published by a government or third party. Valores / values: `public_page`, `public_api`, `retailer_feed`, `open_data`. |
| `quality_status` | `string` | `warning` when a validation flagged the observation (price jump, zero, unusual unit). Quarantined observations are never served. Valores / values: `ok`, `warning`. |
| `quality_flags` | `string[]` | Why the price was flagged. The price is delivered as published, never corrected. `discount_over_85`: more than 85% off the list price. `round_price_low`: a round price of CLP 1,000 or less. Both often mean a placeholder price or a store error. |
| `corrected` | `boolean` | True when the observation was amended after capture (the original is kept). |
| `converted` | `Converted` |  |
| `converted.currency` **requerido** | `string` |  |
| `converted.price` **requerido** | `number` |  |
| `converted.list_price` | `number \| null` |  |
| `converted.card_price` | `number \| null` |  |
| `converted.unit_price` | `number \| null` |  |
| `converted.fx_rate` **requerido** | `number` | Units of target currency per unit of source currency. |
| `converted.fx_date` **requerido** | `string (date)` | Publication date of the rate used (last published on or before the observation date). |
| `converted.fx_source` | `string` |  |
| `converted.fx_method` | `string` | Valores / values: `direct`, `cross`. |
| `retailer` **requerido** | `RetailerRef` |  |
| `retailer.id` **requerido** | `string` |  |
| `retailer.name` **requerido** | `string` |  |
| `offer_id` **requerido** | `string` |  |
| `store_id` | `string \| null` | Null when the price is national/online. |
| `url` **requerido** | `string (uri)` |  |
| `title_original` **requerido** | `string` |  |
| `seller` | `Seller` | Who sells the offer. On marketplaces it is a third party; `name` is null when the seller is a natural person (no personal data). |
| `seller.id` **requerido** | `string` |  |
| `seller.name` **requerido** | `string \| null` |  |
| `seller.is_retailer_itself` **requerido** | `boolean` |  |
| `match` **requerido** | `Match` | How the offer was linked to the canonical product. |
| `match.method` **requerido** | `string` | `singleton`: the offer matched nothing else and is its own product. Valores / values: `gtin`, `rules`, `model`, `manual`, `singleton`. |
| `match.confidence` **requerido** | `number` |  |
| `match.kind` **requerido** | `string` | `identical`: the same item. `pack`: the offer is N units of the product (`pack_ratio`), comparable by unit price. `equivalent`: a declared substitute of another brand or lab, not the same item. Valores / values: `identical`, `pack`, `equivalent`. |
| `match.pack_ratio` | `number \| null` | Units of the product in the offer, for kind pack. |

### Producto (`Product`)

| Campo | Tipo | Descripción |
|---|---|---|
| `id` **requerido** | `string` |  |
| `name` **requerido** | `string` |  |
| `brand` **requerido** | `BrandRef` |  |
| `brand.id` **requerido** | `string` |  |
| `brand.name` **requerido** | `string` |  |
| `category` **requerido** | `CategoryRef` |  |
| `category.id` **requerido** | `string` |  |
| `category.label` **requerido** | `string` |  |
| `category.path` | `string[]` |  |
| `size` | `object` |  |
| `gtin` | `string[]` |  |
| `image_url` | `string \| null` |  |
| `price_summary` **requerido** | `PriceSummary` |  |
| `price_summary.currency` **requerido** | `string` |  |
| `price_summary.min` **requerido** | `number \| null` |  |
| `price_summary.max` **requerido** | `number \| null` |  |
| `price_summary.median` | `number \| null` |  |
| `price_summary.offers_count` **requerido** | `integer` |  |
| `price_summary.retailers_count` **requerido** | `integer` |  |
| `price_summary.best` | `object \| null` |  |
| `price_summary.observed_at` | `string (date-time) \| null` |  |
| `price_summary.converted` | `object` |  |
| `score` | `number` | Relevance score, search only. |
| `names` | `object` | Name in every available language. |
| `names_meta` | `object` | Provenance of each name, keyed by language. |
| `private_label` | `boolean` | Store brand of one retailer. |
| `relations` | `object[]` | Links to other canonical products. |
| `relations[].kind` **requerido** | `string` | Valores / values: `pack_of`, `size_of`, `variant_of`, `equivalent`, `replaced_by`. |
| `relations[].product_id` **requerido** | `string` |  |
| `relations[].quantity_ratio` **requerido** | `number \| null` | For pack_of and size_of, how many units of product_id this product contains. |
| `identifiers` | `object` |  |
| `identifiers.gtin` | `string[]` |  |
| `identifiers.mpn` | `string \| null` |  |
| `attributes` | `object` |  |
| `countries` | `string[]` |  |
| `first_seen` | `string (date-time)` |  |

### Precio condicional (`ConditionalPrice`)

| Campo | Tipo | Descripción |
|---|---|---|
| `kind` **requerido** | `string` | Valores / values: `payment_method`, `membership`, `app_only`, `coupon`, `other`. |
| `label` **requerido** | `string` | As shown by the retailer. |
| `price` **requerido** | `number` |  |
| `payment_method` | `string \| null` |  |
| `membership_tier` | `string \| null` |  |
| `installments_count` | `integer \| null` |  |
| `interest_free` | `boolean \| null` |  |

### Promoción (`Promotion`)

| Campo | Tipo | Descripción |
|---|---|---|
| `type` **requerido** | `string` | Valores / values: `multi_buy`, `nth_unit`, `percent_off`, `amount_off`, `bundle`, `gift`, `other`. |
| `label` **requerido** | `string` | As shown by the retailer, e.g. "2 x $2.990". |
| `min_quantity` | `integer \| null` |  |
| `effective_unit_price` | `number \| null` | Price per item when the promotion applies. |
| `requires_card` | `string \| null` | Payment method required, if any. |
| `valid_until` | `string (date) \| null` |  |

### Atribución (`Attribution`)

| Campo | Tipo | Descripción |
|---|---|---|
| `text` **requerido** | `string` |  |
| `source` **requerido** | `string` | Brand name. |
| `as_of` **requerido** | `string (date-time)` |  |
| `resource_id` **requerido** | `string \| null` | Product or offer the response is about |
| `url` **requerido** | `string (uri) \| null` | Public page for the product |
| `capture_method` | `string \| null` | Set when the response is about one offer. Valores / values: `public_page`, `public_api`, `retailer_feed`, `open_data`. |
| `source_url` | `string (uri) \| null` | Retailer URL of the offer, when the response is about one offer. |

### Conversión (`Converted`)

| Campo | Tipo | Descripción |
|---|---|---|
| `currency` **requerido** | `string` |  |
| `price` **requerido** | `number` |  |
| `list_price` | `number \| null` |  |
| `card_price` | `number \| null` |  |
| `unit_price` | `number \| null` |  |
| `fx_rate` **requerido** | `number` | Units of target currency per unit of source currency. |
| `fx_date` **requerido** | `string (date)` | Publication date of the rate used (last published on or before the observation date). |
| `fx_source` | `string` |  |
| `fx_method` | `string` | Valores / values: `direct`, `cross`. |

### Punto del historial (`HistoryPoint`)

| Campo | Tipo | Descripción |
|---|---|---|
| `t` **requerido** | `string` | Date (`day`, `week`, `month` start) or timestamp (`raw`). |
| `price` **requerido** | `number` |  |
| `list_price` **requerido** | `number` |  |
| `sale_price` | `number \| null` |  |
| `card_price` | `number \| null` |  |
| `availability` **requerido** | `string` | Valores / values: `in_stock`, `out_of_stock`, `limited`, `unknown`. |
| `converted` | `object` | Present when `currency` is set. Converted with the rate of the point date (or fx_date). |
| `converted.price` | `number` |  |
| `converted.list_price` | `number` |  |
| `converted.fx_rate` | `number` |  |
| `converted.fx_date` | `string (date)` |  |
