Comercio
Flujo completo de cotizaciones (RFQ), gestión de clientes y roles de compradores
El módulo de comercio cubre la relación con los compradores: autenticación de clientes, sistema de cotizaciones (RFQ) con negociación bidireccional, roles y permisos granulares, y gestión de direcciones.
Gestión de clientes
Crear un cliente
Los clientes se crean desde la API admin con un API token (pk_*):
curl -X POST "https://tu-instancia.com/api/v1/customers" \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-d '{
"firstName": "Carlos",
"lastName": "Mendoza",
"email": "cmendoza@empresa.mx",
"phone": "+52 55 1234 5678",
"language": "es-MX",
"password": "Contraseña123$"
}'El cliente inicia en estado pending. Cámbialo a active para que pueda autenticarse:
curl -X PATCH "https://tu-instancia.com/api/v1/customers/{customerId}" \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-d '{"status": "active"}'Asignar un rol al cliente
Los roles controlan qué puede hacer el comprador en el portal:
curl -X POST "https://tu-instancia.com/api/v1/customers/{customerId}/roles" \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-d '{"roleId": "clx..."}'Los 4 roles predefinidos son:
| Rol | Permisos principales |
|---|---|
| Admin | Acceso total: cotizaciones, direcciones, catálogo |
| Aprobador | Ver y aprobar/rechazar cotizaciones, negociar |
| Comprador | Crear, enviar, negociar y cancelar cotizaciones |
| Visor | Solo lectura |
Actualización de perfil (autogestión)
Un comprador autenticado puede actualizar su propio nombre y teléfono sin pasar por el administrador:
curl -X PUT "https://tu-instancia.com/api/v1/customers/me" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{
"firstName": "Carlos",
"lastName": "Mendoza",
"phone": "+52 55 9876 5432"
}'Campos permitidos: firstName, lastName, phone (puede ser null para borrar el teléfono). El rol, estado e idioma son campos de solo lectura para el comprador; cualquier cambio en esas propiedades debe hacerlo un administrador via PUT /v1/customers/{id}.
Flujo de cotizaciones
Visión general
El flujo RFQ tiene dos lados: el vendedor (API admin) y el comprador (buyer portal). Ambas perspectivas comparten el mismo modelo de datos.
COMPRADOR VENDEDOR
│ │
├─── Crear draft ────►│
├─── Submit ──────────►│
│ ├─── Revisar
│ ├─── Responder con precios
│◄──── responded ────┤
├─── Negociar ────────►│
│◄──── negotiating ──┤
│ ├─── Responder
│◄──── responded ────┤
├─── Aprobar / Rechazar
│Estados posibles
| Estado | Quién lo genera | Descripción |
|---|---|---|
draft | Cualquier parte | En construcción, no enviado |
submitted | Comprador (al enviar) o Vendedor (al responder) | Esperando acción de la otra parte |
negotiating | Comprador | El comprador hizo una contraoferta; el vendedor debe responder |
approved | Comprador | Todos los ítems fueron aceptados |
partially_approved | Comprador | Algunos ítems aceptados, otros rechazados |
rejected | Comprador | El comprador rechazó la propuesta del vendedor |
declined | Vendedor | El vendedor rechazó la solicitud del comprador |
expired | Sistema | Venció sin resolución |
cancelled | Cualquier parte | Cancelado explícitamente |
converted | Vendedor | La cotización aprobada fue convertida a pedido |
rejectedvsdeclined: cuando el comprador rechaza una propuesta del vendedor, el estado esrejectedy el comprador puede reenviarla. Cuando el vendedor rechaza la solicitud (endpointPOST /v1/quotes/{id}/reject), el estado esdeclined.
Campo awaitingResponseFrom
Todas las cotizaciones incluyen el campo awaitingResponseFrom ("buyer", "seller" o null) que indica de forma explícita de quién se espera la siguiente acción:
| Valor | Significado |
|---|---|
"seller" | La cotización está en manos del vendedor (estado submitted recién enviado por el comprador, o negotiating) |
"buyer" | El vendedor respondió y espera que el comprador actúe |
null | La cotización está resuelta (aprobada, rechazada, cancelada, expirada) |
Usa este campo en tu UI para decidir qué acciones mostrar al usuario sin necesidad de interpretar el estado.
Mensajes en la cotización
Todas las cotizaciones incluyen un arreglo messages con el historial de mensajes intercambiados durante la negociación:
{
"messages": [
{
"id": "clx...",
"authorType": "buyer",
"authorId": "clx...",
"authorName": "Carlos Mendoza",
"message": "¿Podría ajustar el precio si compramos 200 unidades?",
"createdAt": "2024-01-15T12:30:00Z"
},
{
"id": "clx...",
"authorType": "seller",
"authorId": "clx...",
"authorName": "Ana López",
"message": "Ajustamos el precio para 200 unidades",
"createdAt": "2024-01-16T09:00:00Z"
}
]
}Los mensajes se agregan automáticamente al usar los campos message en los endpoints /negotiate y /respond. El campo authorType es "buyer" o "seller".
Cotizaciones desde el comprador
Todos los endpoints del portal de comprador requieren autenticación de comprador. Consulta la guía de autenticación para obtener el JWT del comprador.
Los tres headers requeridos son:
X-Storefront-SlugAuthorization: Bearer sf_*X-Buyer-Token: eyJ...
Crear una cotización
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{
"notes": "Necesito estos productos para el próximo mes"
}'Respuesta:
{
"data": {
"id": "clx...",
"quoteNumber": "QT-0006",
"status": "draft",
"items": [],
"createdAt": "2024-01-15T10:00:00Z"
}
}Agregar ítems a la cotización
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/items" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{
"productId": "clx...",
"variantId": "clx...",
"quantity": 100,
"notes": "Color preferido: azul marino"
}'Asignar dirección de envío
Opcionalmente puedes asociar una dirección de envío a la cotización. La dirección debe pertenecer a la empresa del comprador (ver sección Direcciones):
curl -X PATCH "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{"shippingAddressId": "clx_address_id"}'La respuesta de la cotización incluye el objeto completo de la dirección en el campo shippingAddress (y billingAddress si el vendedor la asoció), con todos sus campos (street, city, state, country, etc.).
Enviar la cotización
Una vez que la cotización tiene ítems, envíala al vendedor:
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/submit" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..."Consultar estado de la cotización
curl "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..."Negociar (contraoferta)
Cuando el vendedor responde, puedes negociar proponiendo precios o cantidades alternativos:
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/negotiate" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{
"message": "¿Podría ajustar el precio si compramos 200 unidades?",
"items": [
{
"itemId": "clx...",
"proposedPrice": 320.00,
"proposedQuantity": 200
}
]
}'El body debe incluir al menos un message o al menos un elemento en items. Cada ítem en items debe tener al menos proposedPrice o proposedQuantity (o ambos).
Al negociar, la cotización pasa a estado negotiating y awaitingResponseFrom queda como "seller". La fecha de expiración se reinicia automáticamente.
Límite de rondas: cada respuesta del vendedor incrementa el contador negotiationRound. Cuando el comprador intenta negociar y ya se alcanzó el límite configurado (por defecto 10), el sistema devuelve el error QUOTE_NEGOTIATION_LIMIT_EXCEEDED.
Aprobar la cotización
Aprobación total (acepta todos los ítems):
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/approve" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..."Aprobación parcial (selecciona qué ítems aceptar):
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/approve" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "itemId": "clx_item_1", "accepted": true },
{ "itemId": "clx_item_2", "accepted": false }
]
}'Si todos los ítems tienen accepted: true, el estado final es approved. Si hay al menos uno en false, el estado es partially_approved. Cada ítem refleja su propio estado en el campo status (accepted o rejected).
Rechazar la cotización
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/reject" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{"reason": "Los precios no son competitivos"}'Reenviar una cotización rechazada
Si el vendedor rechazó (declined) tu cotización, puedes reenviarla:
curl -X POST "https://tu-instancia.com/api/v1/buyer/quotes/{quoteId}/resubmit" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{"message": "Actualizamos nuestros requerimientos"}'Cada reenvío incrementa el contador revisionCount. Cuando se alcanza el límite configurado (por defecto 3), el sistema devuelve el error QUOTE_REVISION_LIMIT_EXCEEDED y no permite más reenvíos.
revisionCountvsnegotiationRound: son contadores distintos con propósitos distintos.revisionCountcuenta cuántas veces el comprador reenvió la cotización tras un rechazo del vendedor (declined).negotiationRoundcuenta cuántas veces el vendedor respondió formalmente con precios (ya sea en la respuesta inicial o en respuestas posteriores a una contraoferta). Ambos tienen límites independientes configurables por empresa.
Listar todas las cotizaciones del comprador
curl "https://tu-instancia.com/api/v1/buyer/quotes" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..."Filtros disponibles: status, page, limit, sort_by (created_at | submitted_at | quote_number), sort_order (asc | desc).
Cotizaciones desde el vendedor
El equipo de ventas gestiona cotizaciones desde la API admin con un token pk_*.
Listar cotizaciones
curl "https://tu-instancia.com/api/v1/quotes?status=submitted&awaiting_response_from=seller" \
-H "Authorization: Bearer pk_..."Filtros disponibles:
| Parámetro | Tipo | Descripción |
|---|---|---|
status | string | Filtrar por estado (draft, submitted, negotiating, etc.) |
customer_id | UUID | Filtrar por cliente específico |
awaiting_response_from | buyer | seller | Filtrar por quién debe actuar a continuación |
search | string | Buscar en número, título o referencia de la cotización |
date_from | ISO 8601 | Fecha de creación desde |
date_to | ISO 8601 | Fecha de creación hasta |
sort_by | string | created_at (default), submitted_at, quote_number, status |
sort_order | string | asc | desc (default: desc) |
page | number | Página (default: 1) |
limit | number | Resultados por página, máx 250 (default: 25) |
Responder a una cotización
curl -X POST "https://tu-instancia.com/api/v1/quotes/{quoteId}/respond" \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-d '{
"discount": 1600.00,
"shippingCost": 500.00,
"tax": 4944.00,
"total": 35844.00,
"sellerNotes": "Precio especial por volumen aplicado",
"items": [
{
"itemId": "clx...",
"unitPrice": 320.00,
"quantity": 100
}
]
}'Los campos financieros (discount, shippingCost, tax, total) deben proporcionarse todos juntos o ninguno. El subtotal se calcula automáticamente a partir de los precios unitarios de los ítems y no debe enviarse en el cuerpo.
Aprobar o rechazar como vendedor
# Aprobar (transición a estado `approved`)
curl -X POST "https://tu-instancia.com/api/v1/quotes/{quoteId}/approve" \
-H "Authorization: Bearer pk_..."
# Rechazar (transición a estado `declined`)
curl -X POST "https://tu-instancia.com/api/v1/quotes/{quoteId}/reject" \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-d '{"reason": "Producto descontinuado"}'El endpoint del vendedor se llama
/rejectpero el estado resultante esdeclined, norejected. Esto distingue el rechazo del vendedor (declined) del rechazo del comprador (rejected).
Responder a una contraoferta
Cuando el comprador negocia (estado negotiating), el vendedor responde con el mismo endpoint /respond:
curl -X POST "https://tu-instancia.com/api/v1/quotes/{quoteId}/respond" \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-d '{
"discount": 3200.00,
"shippingCost": 500.00,
"tax": 9744.00,
"total": 71044.00,
"sellerNotes": "Ajustamos el precio para 200 unidades",
"items": [
{
"itemId": "clx...",
"unitPrice": 320.00,
"quantity": 200
}
]
}'Esto incrementa negotiationRound y regresa la cotización a estado submitted con awaitingResponseFrom: "buyer".
Descargar cotización en PDF
curl "https://tu-instancia.com/api/v1/quotes/{quoteId}/pdf" \
-H "Authorization: Bearer pk_..." \
--output cotizacion.pdfEl PDF incluye los datos de la cotización, ítems con precios, totales y los términos y condiciones configurados para la empresa (clave de configuración pdf_terms_and_conditions). Solo está disponible para cotizaciones en estados: submitted, negotiating, approved, partially_approved y converted.
Direcciones
Los compradores pueden gestionar las direcciones de envío de su empresa:
# Listar direcciones
curl "https://tu-instancia.com/api/v1/buyer/addresses" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..."
# Crear dirección
curl -X POST "https://tu-instancia.com/api/v1/buyer/addresses" \
-H "X-Storefront-Slug: acme-b2c" \
-H "Authorization: Bearer sf_dev_seed_acme_b2c_token" \
-H "X-Buyer-Token: eyJ..." \
-H "Content-Type: application/json" \
-d '{
"label": "Bodega Principal",
"contactName": "Juan García",
"contactPhone": "+52 55 9876 5432",
"street": "Av. Industrial",
"streetNumber": "456",
"city": "Ciudad de México",
"state": "CDMX",
"country": "MX",
"postalCode": "06600",
"isShipping": true,
"isBilling": false
}'