Order Completed
Una orden de compra se completó exitosamente. Es el evento de conversión del negocio.
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.
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.
{ "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
| Campo | Detalle |
|---|---|
price, amount_payed_with_credit, items[].price | Viajan como string, no como número. El consumidor los castea a float. |
order_id, item_id, analytics_client.internal_id | Ids numéricos serializados como string. |
channel | El consumidor descarta el evento si no es web. |
analytics_client | Forma abierta: se arma con **data_session, un JSON libre. Puede traer claves no documentadas; Avro las ignora al validar. |
event_source_url | Hoy siempre string vacío (hardcodeado en el productor). |
items | El consumidor exige al menos un item; el productor no lo garantiza. |
occurred_at | ISO-8601 naive, hora local del proceso. No confundir con el header time, que sí es UTC. |
Consumidores
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/eventsdeanalytics: un cambio de schema impacta en los dos caminos.