event

Product Created

Se creó un producto (Activity o Box) elegible para indexarse. Lleva el documento completo, no solo el id.

EventsearchPayload completo

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 Activity o Box.

  2. Serializa

    Con ActivitySerializer o BoxSerializer según el tipo.

  3. Evalúa si corresponde indexar

    should_index aplica los filtros de ProductDocument (excluye los inactive).

  4. Elige el evento

    should_index + created=Trueeste evento. should_index sin createdproduct.updated. Sin should_indexproduct.deleted.

Warning

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.

search.product.created.v1 — Avro
{
"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.

CampoDetalle
typeDiscriminador. Un valor fuera de Activity/Box hace que el consumidor lance y el mensaje se nackee.
reviews_qtyLlega null para Box, pero el BoxDocument del consumidor lo declara int = 0, que no acepta None. Ver deuda conocida.
created_at / updated_atEl consumidor los declara, pero nunca viajan en el evento.
has_deal_todaySiempre false para Box (los deals son solo de actividades).
occurred_atISO-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_qty null en Box: por lectura del código, los eventos de Box fallarían la validación del consumidor y quedarían logueados como Invalid 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).