Product Created
Se creó un producto (Activity o Box) elegible para indexarse. Lleva el documento completo, no solo el id.
Overview
search.product.created.v1 se emite cuando se crea un producto —una Activity o un Box—
que cumple las condiciones para estar indexado. El payload es el documento entero del
producto, listo para hacer upsert en Elasticsearch sin volver a consultar al monolito.
El payload es idéntico al de search.product.updated.
Cuándo se dispara
Desde la señal post_save de Django sobre cualquier Product
(search.signals.products_post_save):
- 1
Filtra por tipo
Solo actúa si la instancia es
ActivityoBox. - 2
Serializa
Con
ActivitySerializeroBoxSerializersegún el tipo. - 3
Evalúa si corresponde indexar
should_indexaplica los filtros deProductDocument(excluye losinactive). - 4
Elige el evento
should_index+created=True→ este evento.should_indexsincreated→product.updated. Sinshould_index→product.deleted.
Toda la función está envuelta en un try/except que solo reporta a New Relic: un error al
serializar se traga en silencio y el producto nunca se indexa.
Schema
Formato Avro, validado con fastavro.
{ "type": "record", "name": "ProductCreated", "namespace": "search.product", "doc": "Emitido cuando se crea un producto indexable (Activity o Box) en bigbox-backend. Payload = documento de producto serializado, identico al de search.product.updated.v1. Consumido por el servicio search para indexar en Elasticsearch.", "fields": [ { "name": "id", "type": "int", "doc": "Id del producto." }, { "name": "type", "type": { "type": "enum", "name": "ProductType", "symbols": ["Activity", "Box"] }, "doc": "Discriminador. El consumer rutea a ActivityDocument o BoxDocument segun este campo; un valor desconocido lanza error y el mensaje se nackea." }, { "name": "country_id", "type": ["null", "int"], "default": null, "doc": "Id del pais del producto." }, { "name": "name", "type": "string", "doc": "Nombre del producto." }, { "name": "description", "type": ["null", "string"], "default": null }, { "name": "short_description", "type": ["null", "string"], "default": null }, { "name": "price", "type": ["null", "double"], "default": null, "doc": "Precio. En Box se toma del campo de precio del pais (Box.price_fields[country_id])." }, { "name": "participants", "type": { "type": "array", "items": "string" }, "doc": "Buckets de participantes: '1', '2', '2+'." }, { "name": "landing_slugs", "type": { "type": "array", "items": "string" }, "doc": "Slugs de landings donde aparece el producto (directas + por product tag)." }, { "name": "category", "type": [ "null", { "type": "record", "name": "Category", "fields": [ { "name": "id", "type": ["null", "int"], "default": null }, { "name": "name", "type": "string" }, { "name": "slug", "type": "string" } ] } ], "default": null }, { "name": "category_visible", "type": "boolean" }, { "name": "slug", "type": "string" }, { "name": "most_sold", "type": "boolean" }, { "name": "most_sold_order", "type": ["null", "int"], "default": null }, { "name": "rating", "type": ["null", "double"], "default": null, "doc": "score_satisfaction del producto." }, { "name": "individual_purchase_order", "type": ["null", "int"], "default": null }, { "name": "units_sold_last_30_days", "type": "int" }, { "name": "units_validated_last_30_days", "type": "int" }, { "name": "campaign_slugs", "type": { "type": "array", "items": "string" }, "doc": "Slugs de todas las campanas activas que incluyen al producto. No implica descuento." }, { "name": "product_tag_slugs", "type": { "type": "array", "items": "string" } }, { "name": "has_campaign_discount", "type": "boolean", "doc": "True si el producto esta en al menos una campana activa con descuento > 0." }, { "name": "has_deal_today", "type": "boolean", "doc": "True si hay un ActivityDiscount aprobado aplicable hoy. Siempre false para Box." },
{ "name": "individual_purchase_available", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "visible", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "box_names", "type": ["null", { "type": "array", "items": "string" }], "default": null, "doc": "Solo Activity: nombres de los boxes que la contienen. null cuando type = Box." }, { "name": "box_ids", "type": ["null", { "type": "array", "items": "int" }], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "partner_fantasy_name", "type": ["null", "string"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "has_online_reservation", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "reviews_qty", "type": ["null", "int"], "default": null, "doc": "Solo Activity (survey_qty). null cuando type = Box — ver observaciones del .md." }, { "name": "visible_catalog", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "country_delivery", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "is_online_activity", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "can_purchase_a_reservation", "type": ["null", "boolean"], "default": null, "doc": "Solo Activity. null cuando type = Box." }, { "name": "activity_type", "type": ["null", "string"], "default": null, "doc": "Solo Activity: Activity | PartnerActivity | ComboActivity | TravelActivity. null cuando type = Box." }, { "name": "province", "type": ["null", { "type": "array", "items": { "type": "record", "name": "Province", "fields": [ { "name": "id", "type": "int" }, { "name": "name", "type": "string" }, { "name": "slug", "type": "string" } ] } }], "default": null, "doc": "Provincias del producto. Activity: derivadas de sus locations (o todas las del pais si es online / country_delivery). Box: derivadas de sus activities activas." }, { "name": "cities", "type": ["null", { "type": "array", "items": { "type": "record", "name": "City", "fields": [ { "name": "id", "type": "int" }, { "name": "name", "type": "string" }, { "name": "province_id", "type": ["null", "int"], "default": null } ] } }], "default": null }, { "name": "subfilters", "type": ["null", { "type": "array", "items": { "type": "record", "name": "SubFilter", "fields": [ { "name": "id", "type": "int" }, { "name": "name", "type": "string" }, { "name": "parent_id", "type": ["null", "int"], "default": null }, { "name": "parent_name", "type": ["null", "string"], "default": null }, { "name": "parent_order", "type": ["null", "int"], "default": null } ] } }], "default": null }, { "name": "availability", "type": ["null", { "type": "array", "items": { "type": "record", "name": "AvailabilitySlot", "doc": "Slots con disponibilidad desde hoy hasta el maximo rango de ActivityAvailabilityService.", "fields": [ { "name": "location_id", "type": "int" }, { "name": "province_id", "type": ["null", "int"], "default": null }, { "name": "city_id", "type": ["null", "int"], "default": null }, { "name": "date", "type": ["null", "string"], "default": null }, { "name": "time", "type": ["null", "string"], "default": null }, { "name": "bookable_until", "type": ["null", "string"], "default": null } ] } }], "default": null, "doc": "Solo Activity. null cuando type = Box." },
{ "name": "has_an_activity_with_visible_catalog", "type": ["null", "boolean"], "default": null, "doc": "Solo Box. null cuando type = Activity." }, { "name": "activities_count", "type": ["null", "int"], "default": null, "doc": "Solo Box. null cuando type = Activity." }, { "name": "activities_visible_catalog_count", "type": ["null", "int"], "default": null, "doc": "Solo Box. null cuando type = Activity." },
{ "name": "occurred_at", "type": "string", "doc": "ISO-8601 naive en hora local del proceso. Lo inyecta events.event.Event.__init__." } ]}Un payload, dos formas
El serializador declara los campos de los dos tipos de producto y rellena con null los
que no aplican. Un Box llega con availability: null, box_ids: null, activity_type: null…; una Activity llega con activities_count: null. Por eso casi todos los campos
específicos son nullables en el schema.
| Campo | Detalle |
|---|---|
type | Discriminador. Un valor fuera de Activity/Box hace que el consumidor lance y el mensaje se nackee. |
reviews_qty | Llega null para Box, pero el BoxDocument del consumidor lo declara int = 0, que no acepta None. Ver deuda conocida. |
created_at / updated_at | El consumidor los declara, pero nunca viajan en el evento. |
has_deal_today | Siempre false para Box (los deals son solo de actividades). |
occurred_at | ISO-8601 naive, hora local del proceso. |
Consumidores
search parsea a ActivityDocument o BoxDocument según type y hace upsert en el
índice products. Ambos modelos usan extra="allow", así que agregar campos al payload no
rompe nada.
Deuda conocida
reviews_qtynull en Box: por lectura del código, los eventos de Box fallarían la validación del consumidor y quedarían logueados comoInvalid payload. Conviene contrastarlo con logs antes de tocar nada.- Producir este evento es caro: el serializador ejecuta varias queries por producto
(
province,cities,subfilters,availability).