event

Order Completed

Una orden de compra se completó exitosamente. Es el evento de conversión del negocio.

EventpaymentsContiene PII

Overview

payments.order.completed.v1 se emite cuando una orden de compra se completa exitosamente. Es el evento de conversión: de él dependen GA4, Meta, Google Ads y Customer.io. Si este evento deja de fluir, se cae la atribución de todo el negocio.

Contiene datos personales

El payload incluye email, teléfono, nombre y apellido del comprador, además de identificadores de sesión publicitaria. Tratarlo como dato sensible en logs, en almacenamiento y ante cualquier consumidor nuevo.

Cuándo se dispara

Desde Order.complete() (src/payments/models.py), dentro de un transaction.on_commit(...) — o sea, solo si la transacción que completa la orden llega a commitear.

Un guard previo evita el doble disparo cuando la orden ya está en estado success.

Schema

Formato Avro, validado con fastavro.

payments.order.completed.v1 — Avro
{
"type": "record",
"name": "OrderCompleted",
"namespace": "payments.order",
"doc": "Emitido cuando una orden se completa exitosamente en bigbox-backend (payments). Consumido por el servicio analytics para reenviar la conversion a GA4, Meta, Google Ads y Customer.io.",
"fields": [
{
"name": "order_id",
"type": "string",
"doc": "Id de la orden. Se emite como string aunque el id sea numerico (str(order.id))."
},
{
"name": "channel",
"type": ["null", "string"],
"default": null,
"doc": "Canal de la orden (Order.CHANNEL): biglife | corporate_send | corporative | internal | maleva | manual_corporative | stock_entry | web. analytics descarta todo lo que no sea 'web'."
},
{
"name": "price",
"type": "string",
"doc": "Precio final de la orden. Se emite como string (str(order.get_final_price())); el consumer lo castea a float >= 0."
},
{
"name": "currency",
"type": {
"type": "enum",
"name": "Currency",
"symbols": ["USD", "ARS", "UYU", "PEN", "CLP", "COP", "MXN", "EUR"]
},
"doc": "Codigo de moneda derivado del pais de la orden (CurrentSiteMapper.currency_code). Los simbolos son los que acepta el consumer."
},
{
"name": "items",
"type": {
"type": "array",
"items": {
"type": "record",
"name": "Item",
"fields": [
{
"name": "item_id",
"type": "string",
"doc": "Id del producto, emitido como string."
},
{
"name": "item_name",
"type": "string",
"doc": "Nombre del producto."
},
{
"name": "short_description",
"type": ["null", "string"],
"default": null,
"doc": "Descripcion corta del producto hijo, si existe."
},
{
"name": "price",
"type": "string",
"doc": "Precio del item, emitido como string; el consumer lo castea a float."
},
{
"name": "quantity",
"type": "int",
"doc": "Siempre 1: se emite una entrada por item de la orden."
},
{
"name": "currency",
"type": "Currency",
"doc": "Misma moneda que la orden."
},
{
"name": "shipping_type",
"type": ["null", "string"],
"default": null,
"doc": "typename del shipping del item; null si el item no tiene shipping."
}
]
}
},
"doc": "Items activos de la orden (order_line.date_removed is null). El consumer exige al menos 1."
},
{
"name": "email",
"type": "string",
"doc": "Email del comprador. El consumer lo valida como EmailStr y aplica una whitelist de dominios."
},
{
"name": "phone",
"type": ["null", "string"],
"default": null,
"doc": "Telefono del comprador."
},
{
"name": "first_name",
"type": ["null", "string"],
"default": null,
"doc": "Nombre del comprador."
},
{
"name": "last_name",
"type": ["null", "string"],
"default": null,
"doc": "Apellido del comprador."
},
{
"name": "event_source_url",
"type": ["null", "string"],
"default": null,
"doc": "URL de origen de la conversion. Hoy el publisher siempre envia string vacio."
},
{
"name": "country_id",
"type": ["null", "int"],
"default": null,
"doc": "Id del pais de la orden."
},
{
"name": "coupon",
"type": ["null", "string"],
"default": null,
"doc": "Codigo de descuento aplicado; null si no hubo."
},
{
"name": "items_with_change_or_extension",
"type": ["null", "boolean"],
"default": null,
"doc": "True si algun item de la orden es una Extension (cambio o extension)."
},
{
"name": "amount_payed_with_credit",
"type": ["null", "string"],
"default": null,
"doc": "Monto pagado con credito, emitido como string (str(order.get_total_credit()))."
},
{
"name": "analytics_client",
"type": {
"type": "record",
"name": "AnalyticsClient",
"doc": "Identidad del cliente para analytics. Se arma con los campos fijos mas el spread de AnalyticsSession.data_session, por lo que puede traer claves extra no listadas aca.",
"fields": [
{
"name": "internal_id",
"type": ["null", "string"],
"default": null,
"doc": "Id interno del usuario, emitido como string."
},
{
"name": "ga4_client_id",
"type": ["null", "string"],
"default": null,
"doc": "client_id de GA4 tomado de data_session."
},
{
"name": "gclid",
"type": ["null", "string"],
"default": null,
"doc": "Google Click Id."
},
{
"name": "ga_user_properties",
"type": ["null", { "type": "map", "values": ["null", "string", "boolean", "double"] }],
"default": null,
"doc": "User properties libres que se reenvian a GA4."
},
{
"name": "ga_session",
"type": [
"null",
{
"type": "record",
"name": "GASession",
"fields": [
{ "name": "ga_session_id", "type": ["null", "string"], "default": null },
{ "name": "ga_session_number", "type": ["null", "string"], "default": null }
]
}
],
"default": null
},
{
"name": "meta_session",
"type": [
"null",
{
"type": "record",
"name": "MetaSession",
"fields": [
{ "name": "fbp", "type": ["null", "string"], "default": null },
{ "name": "fbc", "type": ["null", "string"], "default": null },
{ "name": "gclicl", "type": ["null", "string"], "default": null, "doc": "Nombre tal cual esta en el consumer (posible typo de gclid)." }
]
}
],
"default": null
},
{
"name": "location",
"type": [
"null",
{
"type": "record",
"name": "LocationSession",
"fields": [
{ "name": "client_ip_address", "type": ["null", "string"], "default": null },
{ "name": "client_user_agent", "type": ["null", "string"], "default": null }
]
}
],
"default": null
}
]
},
"doc": "Datos de sesion/identidad del cliente."
},
{
"name": "occurred_at",
"type": "string",
"doc": "ISO-8601 naive en hora local del proceso. Lo inyecta events.event.Event.__init__ en todos los eventos del monolito."
}
]
}

Campos que conviene mirar dos veces

CampoDetalle
price, amount_payed_with_credit, items[].priceViajan como string, no como número. El consumidor los castea a float.
order_id, item_id, analytics_client.internal_idIds numéricos serializados como string.
channelEl consumidor descarta el evento si no es web.
analytics_clientForma abierta: se arma con **data_session, un JSON libre. Puede traer claves no documentadas; Avro las ignora al validar.
event_source_urlHoy siempre string vacío (hardcodeado en el productor).
itemsEl consumidor exige al menos un item; el productor no lo garantiza.
occurred_atISO-8601 naive, hora local del proceso. No confundir con el header time, que sí es UTC.

Consumidores

publishes
event
routes to
routes to
consumes
Viewing
Event
Order Completed(v1)
omega-backend+1
Service
Bigbox Backend(v1.0.0)
omega-backend+2
Channel
Google Cloud Pub/Subv1.0.0
At least oncepubsub
ecommerzeta
Service
Analytics(v1.0.0)
ecommerzeta
  • events (1)
  • services (2)
  • channels (2)

analytics valida contra un DTO de Pydantic, filtra por canal, deduplica por purchase_{order_id} y hace fan-out a los cuatro providers. El DTO renombra dos campos: order_id → transaction_id y price → value; el nombre canónico es el del productor.

Deuda conocida

  • El productor loguea el payload completo en nivel INFO, PII incluida.
  • El mismo contrato está expuesto por HTTP en POST /api/v1/events de analytics: un cambio de schema impacta en los dos caminos.