TunoCommerce

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:

RolPermisos principales
AdminAcceso total: cotizaciones, direcciones, catálogo
AprobadorVer y aprobar/rechazar cotizaciones, negociar
CompradorCrear, enviar, negociar y cancelar cotizaciones
VisorSolo 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

EstadoQuién lo generaDescripción
draftCualquier parteEn construcción, no enviado
submittedComprador (al enviar) o Vendedor (al responder)Esperando acción de la otra parte
negotiatingCompradorEl comprador hizo una contraoferta; el vendedor debe responder
approvedCompradorTodos los ítems fueron aceptados
partially_approvedCompradorAlgunos ítems aceptados, otros rechazados
rejectedCompradorEl comprador rechazó la propuesta del vendedor
declinedVendedorEl vendedor rechazó la solicitud del comprador
expiredSistemaVenció sin resolución
cancelledCualquier parteCancelado explícitamente
convertedVendedorLa cotización aprobada fue convertida a pedido

rejected vs declined: cuando el comprador rechaza una propuesta del vendedor, el estado es rejected y el comprador puede reenviarla. Cuando el vendedor rechaza la solicitud (endpoint POST /v1/quotes/{id}/reject), el estado es declined.

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:

ValorSignificado
"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
nullLa 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-Slug
  • Authorization: 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.

revisionCount vs negotiationRound: son contadores distintos con propósitos distintos. revisionCount cuenta cuántas veces el comprador reenvió la cotización tras un rechazo del vendedor (declined). negotiationRound cuenta 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ámetroTipoDescripción
statusstringFiltrar por estado (draft, submitted, negotiating, etc.)
customer_idUUIDFiltrar por cliente específico
awaiting_response_frombuyer | sellerFiltrar por quién debe actuar a continuación
searchstringBuscar en número, título o referencia de la cotización
date_fromISO 8601Fecha de creación desde
date_toISO 8601Fecha de creación hasta
sort_bystringcreated_at (default), submitted_at, quote_number, status
sort_orderstringasc | desc (default: desc)
pagenumberPágina (default: 1)
limitnumberResultados 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 /reject pero el estado resultante es declined, no rejected. 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.pdf

El 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
  }'

On this page