Pular para o conteúdo

Itens em conjunto no carrinho

A plataforma Olist Ecommerce permite que na sua loja tenha a opção de adicionar itens em conjunto no carrinho. Isso significa que o cliente pode adicionar diferentes itens de uma única vez no carrinho, sem acarretar erro no Ecommerce.

O uso do recurso itens em conjunto no carrinho é comum em seções de Compre junto, onde a loja oferece produtos similares para os clientes e eles adicionam diferentes itens de uma única vez no carrinho. Veja um exemplo de uso do Compre junto do nosso template 6 na figura a seguir.

1390

Para utilizar essa funcionalidade você precisa habilitá-la em seu template. Habilitamos utilizando um array JavaScript para enviar todos os produtos em uma única requisição, e no array enviamos a quantidade (quantity) e o código identificador do produto (sku). Adicione o código a seguir no seu template para habilitar o recurso.

<script>
const addItemsCombo: function (comboItems) {
var data = {
items: comboItems
}
var form_data = JSON.stringify(data);
return $.ajax({
url: '/carrinho/adicionar/kit',
type: 'POST',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
xhrFields: {
withCredentials: true
},
cache: false,
data: form_data
});
}
$(document).on('submit', '[data-form-kit]', function (event) {
event.preventDefault();
var comboItems = [];
$('[data-product-kit]').each(function(index, el){
if($(el).find('[name="quantity"]').val() > 0){
var item = {
sku: $(el).find('[name="sku"]').val(),
quantity: parseInt($(el).find('[name="quantity"]').val(), 10),
}
comboItems.push(item);
}
});
// console.log('comboItems', comboItems)
addItemsCombo(comboItems)
});
</script>

É possível também utilizar a funcionalidade de itens em conjunto no carrinho com produtos personalizados. Nesse caso, a loja permite que o cliente adicione produtos do tipo personalizados em conjunto no carrinho. No entanto, há uma diferença para habilitar a funcionalidade de itens em conjunto no carrinho. O que muda do código anterior é a variável item, pois enviamos a requisição adicionando (junto com quantity e sku) mais um objeto com os campos referentes a personalização. Para o caso de produtos personalizados, utilize o código a seguir.

<script>
const addItemsCombo: function (comboItems) {
var data = {
items: comboItems
}
var form_data = JSON.stringify(data);
return $.ajax({
url: '/carrinho/adicionar/kit',
type: 'POST',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
xhrFields: {
withCredentials: true
},
cache: false,
data: form_data
});
}
$(document).on('submit', '[data-form-kit]', function (event) {
event.preventDefault();
var comboItems = [];
$('[data-product-kit]').each(function(index, el){
if($(el).find('[name="quantity"]').val() > 0){
var item = {
sku: $(el).find('[name="sku"]').val(),
quantity: parseInt($(el).find('[name="quantity"]').val(), 10),
}
item[$(el).find('input:not([name="sku"]):not([name="quantity"])').attr('name')] = $(el).find('input:not([name="sku"]):not([name="quantity"])').val();
comboItems.push(item);
}
});
// console.log('comboItems', comboItems)
addItemsCombo(comboItems)
});
</script>

🚧 Aplicação da funcionalidade

Note que no exemplo do template 6 o recurso é usado na seção Compre junto que está página de produto. Para utilizar a funcionalidade, lembre-se que:

  • Além de habilitar o recurso você precisa ajustar o front-end da vitrine para adicionar a seção onde deseja.
  • Utilize tags para agrupar produtos relacionados e assim organizar sua seção e melhorar a experiência para o cliente final.

Uma implementação de compre junto normalmente começa na própria página de produto, a partir de uma tag que identifica quais itens participam do agrupamento. Em vez de depender de uma estrutura fixa, a lógica pode ser resumida em três partes:

  1. Identificar, no produto atual, a tag que ativa o compre junto.
  2. Carregar os demais produtos que compartilham essa mesma tag.
  3. Renderizar uma seção que permita selecionar os itens e enviá-los juntos ao carrinho.

Exemplo de identificação da tag e carregamento dos produtos:

{% assign group_shop = product.tags | where: 'type', 'compre-junto' | first %}
{% if group_shop %}
{% load_products tag: group_shop.name per_page: 3 %}
{% endif %}

Nesse modelo, o nome da tag funciona como vínculo entre o produto principal e os demais produtos que devem aparecer na seção.

Para que o compre junto funcione de forma previsível, a implementação precisa manter um contrato mínimo entre a camada de renderização e a rotina de compra.

ElementoPapel na implementação
Tag compre-juntoAgrupa os produtos que participam da seção
Variável de agrupamento, como group_shopAtiva o carregamento dos produtos relacionados
Lista de produtos carregadosDefine os itens que poderão ser selecionados
Bloco HTML de cada itemExpõe os dados usados na seleção e no cálculo
Input name="sku" em cada itemFornece o SKU efetivo que será enviado
Botão final de compraDispara a adição em conjunto
Área de feedbackExibe sucesso, erro ou indisponibilidade

Sem esse contrato, o JavaScript consegue até disparar a requisição, mas não consegue montar o payload com segurança.

Para implementar o compre junto de forma consistente, a lógica de frontend precisa ter acesso, no mínimo, aos seguintes dados:

DadoUso
Tag de agrupamentoDefine quais produtos participam da seção
Referência do produtoEvita duplicar o produto principal
Preço e preço promocionalPermitem recalcular o resumo
DisponibilidadeImpede envio de itens indisponíveis
SKU selecionadoCompõe o payload final
Dados de personalizaçãoPermitem incluir customizations quando necessário

O exemplo abaixo mostra uma referência de implementação do container principal da seção, já alinhada com os elementos lidos pela rotina de compra.

{% if group_shop %}
{% load_products tag: group_shop.name per_page: 3 %}
{% if products.size >= 2 %}
<section class="section group-shop -side container" data-group-shop-section>
<div class='section-wrapper'>
<h3 class="title">
Compre Junto
</h3>
<div class="products">
<div class='swiper' data-swiper-group>
<div class='swiper-wrapper'>
<div class='swiper-slide -plus'>
{% render 'partials/components/product_block_group_shop', product: current_product, showAttributes: true, showDelete: false, mobile: mobile %}
</div>
{% for product in products %}
{% if product.reference != current_product.reference %}
{% assign customs_tag = product.tags | where: 'type', 'personalizacao' | first %}
{% unless customs_tag %}
<div class='swiper-slide -plus'>
{% render 'partials/components/product_block_group_shop', product: product, showAttributes: true, showDelete: true, mobile: mobile %}
</div>
{% endunless %}
{% endif %}
{% endfor %}
</div>
<div class="swiper-pagination"></div>
</div>
<div class="purchase -hidden">
<form class="form-product" data-form-product-group-shop style="display: none;"></form>
<div class="price-wrapper">
<h4 class="text">Compre os produtos por</h4>
<div class="discount-wrapper -hidden">
<span class="discount-percent"></span>
<span class="original-price"></span>
</div>
<span class="price-group"></span>
<span class="warning" data-warning></span>
</div>
<button class="button-default" data-group-shop-add type="button">
<span>Compre junto</span>
</button>
<div class="msg-response">
<span class="msg-error"></span>
</div>
</div>
</div>
</div>
</section>
{% endif %}
{% endif %}

Esse exemplo mostra o container principal da seção com os elementos que a lógica de compra espera encontrar, como data-group-shop-section, [data-swiper-group], .purchase, .price-group, .warning, .discount-wrapper, [data-form-product-group-shop] e [data-group-shop-add].

Cada item precisa expor, no mínimo, referência, preços, disponibilidade e um name="sku" que a rotina de compra consiga ler no momento do envio.

<div class="product-block -group-shop-block {{ class_sold_out }}"
data-product-box="{{ product.id }}"
data-quickview-type="group-shop"
data-prod-group-shop
data-prod-ref="{{ product.reference }}"
data-price="{{ product.price }}"
data-sale-price="{{ product.sale_price }}"
data-available="{{ product.available }}"
data-discount-percent="{{ discount_amount | default: 0 }}"
>
{% if showDelete %}
<button aria-label="Adicionar ou remover produto do compre junto" class="add-to-group">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4773 3.43628L4.82044 9.09313L1.84315 6.11584" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
{% endif %}
<div class="images">
<a aria-label="Ir para o produto {{ product.name }}" href="{{ product.url }}">
{% render 'partials/components/flag', product: product %}
</a>
</div>
<div class="description">
<h3 class="name">
<a aria-label="Ir para o produto {{ product.name }}" href="{{ product.url }}">
{{ product.name }}
</a>
</h3>
<div class="price-wrapper">
<div class="product-price" data-update-price="{{ product.id }}"></div>
</div>
</div>
</div>

Quando esse bloco usa quickview ou outro carregamento de formulário, o ponto obrigatório continua sendo o mesmo: no momento da compra, o item precisa expor um campo name="sku" válido dentro do bloco selecionado.

Além do payload final, o compre junto costuma seguir algumas regras funcionais importantes:

  1. O produto atual define o contexto da seção.
  2. Os demais itens são carregados a partir da mesma tag de agrupamento.
  3. Produtos equivalentes ao principal não devem ser duplicados na lista.
  4. O total exibido depende apenas dos itens selecionados.
  5. Produtos indisponíveis não devem entrar no envio final.
  6. O botão final só deve enviar os itens válidos e selecionados.
  7. O endpoint usado nesse fluxo é POST /carrinho/adicionar/kit.

Em implementações mais completas, a interface também pode recalcular preço total, preço promocional, desconto percentual e mensagens de indisponibilidade conforme a seleção do cliente.

Exemplo prático da lógica de seleção no JavaScript

Seção intitulada “Exemplo prático da lógica de seleção no JavaScript”

Uma forma consistente de implementar esse comportamento é controlar a seleção por instância da seção. Assim, cada seção mantém sua própria lista de itens selecionados e recalcula o resumo visual sempre que o cliente adiciona ou remove um produto.

O trecho abaixo mostra métodos parciais da rotina de compre junto. Eles devem existir dentro do mesmo objeto ou módulo responsável pela seção.

createInstance: function (section) {
return {
section,
products: section.querySelectorAll('[data-prod-group-shop]'),
selectedProducts: [],
swiper: null,
};
},
getElements: function (instance) {
const section = instance.section;
return {
purchase: section.querySelector('.purchase'),
buyButton: section.querySelector('[data-group-shop-add]'),
form: section.querySelector('[data-form-product-group-shop]'),
swiper: section.querySelector('[data-swiper-group]'),
pagination: section.querySelector('.swiper-pagination'),
};
},
handleSelection: function (instance, currentProduct) {
const match = instance.selectedProducts.filter((product) => {
return product.ref == currentProduct.getAttribute('data-prod-ref');
});
if (match.length == 0) {
this.addItemToBuy(instance, currentProduct);
} else {
this.removeItem(instance, currentProduct);
}
},
addItemToBuy: function (instance, product) {
const ref = product.getAttribute('data-prod-ref');
const price = Number(product.getAttribute('data-price'));
const salePrice = Number(product.getAttribute('data-sale-price'));
const available = product.getAttribute('data-available');
const discountPercent = product.getAttribute('data-discount-percent');
const button = product.querySelector('.add-to-group');
if (button) button.classList.add('-group-selected');
instance.selectedProducts.push({
productEl: product,
ref,
price,
salePrice,
available,
discountPercent,
});
this.updateFinalPrice(instance);
},
removeItem: function (instance, product) {
for (let index = 0; index < instance.selectedProducts.length; index++) {
const selectedProduct = instance.selectedProducts[index];
if (selectedProduct.ref == product.getAttribute('data-prod-ref')) {
const button = product.querySelector('.add-to-group');
if (button) button.classList.remove('-group-selected');
instance.selectedProducts.splice(index, 1);
break;
}
}
this.updateFinalPrice(instance);
},
updateFinalPrice: function (instance) {
const selectedProducts = instance.selectedProducts;
const purchase = this.getElements(instance).purchase;
if (selectedProducts.length == 0) {
if (purchase) purchase.classList.add('-hidden');
return;
}
if (!purchase) return;
purchase.classList.remove('-hidden');
let priceTotal = 0.0;
let salePriceTotal = 0.0;
let discountTotal = 0;
let unavailableProducts = 0;
selectedProducts.forEach((product) => {
const prodPrice = parseFloat(product.productEl.getAttribute('data-price'));
const salePrice = parseFloat(product.productEl.getAttribute('data-sale-price'));
const available = product.productEl.getAttribute('data-available');
if (available === 'false') {
unavailableProducts++;
} else if (prodPrice != salePrice) {
priceTotal = priceTotal + prodPrice;
salePriceTotal = salePriceTotal + salePrice;
} else {
priceTotal = priceTotal + prodPrice;
salePriceTotal = salePriceTotal + prodPrice;
}
});
if (priceTotal !== salePriceTotal) {
discountTotal = 100 - (salePriceTotal * 100) / priceTotal;
}
const textEl = purchase.querySelector('.text');
const priceEl = purchase.querySelector('.price-group');
const totalProducts = selectedProducts.length - unavailableProducts;
if (textEl) textEl.innerText = 'Compre os ' + totalProducts + ' produtos por';
if (priceEl) priceEl.innerText = formatMoney(salePriceTotal);
}

Nesse fluxo, os pontos principais são:

  • A seleção é feita por data-prod-ref.
  • O estado fica isolado em selectedProducts dentro da seção atual.
  • O resumo só aparece quando há itens selecionados.
  • O valor final considera indisponibilidade, preço cheio e preço promocional.

Exemplo prático da rotina de compra no JavaScript

Seção intitulada “Exemplo prático da rotina de compra no JavaScript”

Depois que a seleção já foi definida, a rotina de compra pode montar o payload a partir de selectedProducts, ler o sku de cada item, coletar personalizações quando existirem e enviar tudo de uma vez para o carrinho.

O trecho abaixo mostra o método responsável pelo envio da compra. Ele depende do mesmo contexto de instance, getElements(...), formatMoney(...) e Store.addItemResult(...) usados na implementação da seção.

addProducts: async function (instance) {
const elements = this.getElements(instance);
const buyButton = elements.buyButton;
const form = elements.form;
const purchase = elements.purchase;
if (!buyButton || !form || !purchase) return;
let data = {
items: [],
};
for (let index = 0; index < instance.selectedProducts.length; index++) {
const product = instance.selectedProducts[index].productEl;
const sku = product.querySelector('[name=sku]').value;
const available = product.getAttribute('data-available');
let customizations = [];
if (available === 'true') {
const customsEl = product.querySelectorAll('[data-customization]');
if (customsEl.length > 0) {
let hasCustoms = false;
let customs = {};
customsEl.forEach((custom) => {
const type = custom.getAttribute('type');
if (type !== 'checkbox' || type !== 'radio') {
if (custom.value === '') {
custom.setAttribute('disabled', true);
}
}
const disabled = custom.getAttribute('disabled');
let checked = true;
if (typeof custom.checked != 'undefined') checked = custom.checked;
if (checked && disabled !== 'true') {
hasCustoms = true;
customs[custom.getAttribute('data-customization-name')] = custom.getAttribute('value');
}
});
if (hasCustoms) {
customizations.push(customs);
}
}
data.items.push({
sku: sku,
quantity: 1,
customizations,
});
}
}
const json_data = JSON.stringify(data);
const boxResponse = purchase.querySelector('.msg-response');
if (!buyButton.classList.contains('-adding')) {
buyButton.classList.add('-adding');
try {
const response = await fetch('/carrinho/adicionar/kit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: json_data,
});
if (response.ok) {
console.log('success');
const html = await response.text();
Store.addItemResult('produto-adicionado', html, boxResponse, form);
} else {
console.log('error');
console.error('Erro ao adicionar os produtos do compre junto. Status ' + response.status);
Store.addItemResult('erro-adicionar', null, boxResponse, form);
}
} catch (error) {
console.error('Erro ao enviar os produtos do compre junto.');
console.error(error);
Store.addItemResult('erro-adicionar', null, boxResponse, form);
}
buyButton.classList.remove('-adding');
}
}

Nesse fluxo, os pontos principais são:

  • O payload é montado a partir de instance.selectedProducts.
  • O sku é lido diretamente do campo [name=sku].
  • Itens indisponíveis não entram em data.items.
  • Personalizações são adicionadas em customizations.
  • O envio é feito em JSON para POST /carrinho/adicionar/kit.
  • O retorno da compra alimenta a interface usando Store.addItemResult(...).

No fluxo de compre junto, o carrinho recebe um objeto com items, em que cada item representa um produto selecionado pelo cliente.

Exemplo:

{
"items": [
{
"sku": "SKU-LOOK-1",
"quantity": 1,
"customizations": []
},
{
"sku": "SKU-LOOK-2",
"quantity": 1,
"customizations": [
{
"Nome": "Maria",
"Numero": "10"
}
]
}
]
}

Esse payload é enviado em:

POST /carrinho/adicionar/kit

com Content-Type: application/json.

Uma boa implementação de compre junto deve prever, no mínimo, estes estados:

  • Sem Seleção: o resumo do compre junto pode começar oculto.
  • Com Itens Selecionados: o resumo de preço e o botão final ficam visíveis.
  • Com Desconto: a interface pode exibir preço original, preço final e percentual de desconto.
  • Com Itens Indisponíveis: a interface deve informar que há produtos indisponíveis na seleção.
  • Em Erro de Requisição: a seção deve exibir uma mensagem de falha.
  • Em Sucesso: a resposta do carrinho deve atualizar o feedback visual da compra.

Esses estados ajudam a manter o comportamento do compre junto previsível para o cliente final.

Embora ambos agrupem produtos, compre junto e kit nativo são fluxos diferentes.

RecursoComo funcionaEndpoint principal
kit nativo da plataforma (bundle)produto principal com compra consolidada por SKU principal + bundle_skus[]POST /carrinho/adicionar
compre juntoseleção opcional de produtos relacionados enviada como lista de items[]POST /carrinho/adicionar/kit

Se a implementação depende de um produto principal do tipo bundle, o fluxo correto é o de kit nativo. Se a implementação permite que o cliente selecione produtos relacionados manualmente, o fluxo correto é o de compre junto.